# Modifying contours with ContourAdjuster

**URL:** <https://openrndr.discourse.group/t/modifying-contours-with-contouradjuster/645>\
**Category:** Tutorials\
**Created:** [March 31, 2024, 9:02am UTC](https://openrndr.discourse.group/t/modifying-contours-with-contouradjuster/645 "2024-03-31T09:02:21Z")\
**Posts on this page:** 4\
**Page:** 1

<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:** [March 31, 2024, 9:02am UTC](https://openrndr.discourse.group/t/modifying-contours-with-contouradjuster/645/1 "2024-03-31T09:02:21Z")

</div>

A small demo of the contour adjuster. I accidentally found this shape and I found it interesting 🙂

 ![adjust.DemoAdjustContour01-2024-03-31-10.21.03](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/44d78b020f557a0a45dd6fc7bc99a4ccff7fe811.png)

> **imports**
>
> ```kotlin
> import org.openrndr.application
> import org.openrndr.color.ColorRGBa
> import org.openrndr.extra.shapes.adjust.adjustContour
> import org.openrndr.shape.Circle
> import org.openrndr.shape.Rectangle
> 
> ```

```kotlin
fun main() {
    application {
        program {
            extend {
                val cir = adjustContour(Circle(drawer.bounds.center, 200.0).contour) {
                    vertices.forEach {
                        it.rotate(75.0)
                    }
                }
                val rect = adjustContour(Rectangle.fromCenter(drawer.bounds.center, 200.0).contour) {
                    vertices.forEach {
                        it.rotate(75.0)
                    }
                }
                drawer.clear(ColorRGBa.WHITE)
                drawer.stroke = ColorRGBa.BLACK.opacify(0.7)
                drawer.fill = ColorRGBa.PINK
                drawer.contour(cir)
                drawer.fill = ColorRGBa.PINK.shade(1.1)
                drawer.contour(rect)
            }
        }
    }
}

```

This program uses two `adjustContour` operations to alter all vertices in a circle and in a rectangle. The larger shape used to be a circle, which is internally constructed out of 4 bezier curves. The smaller shape was a square with 4 straight segments.

The program iterates over all (4) vertices of each shape, and uses the `rotate` operation to rotate the control points on each side of each vertex, producing what you see in the image. It’s a bit like sculpting 2D shapes.

I will continue exploring the ContourAdjuster and maybe share other interesting results.

---

<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:** [May 26, 2024, 11:37am UTC](https://openrndr.discourse.group/t/modifying-contours-with-contouradjuster/645/2 "2024-05-26T11:37:43Z")

</div>

Another adjuster example:

![Adjust-2024-05-26-13.11.15](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/6e640253f0ca1fb6a243081a74eb9da9936815a8.png)

> **Imports**
>
> ```kotlin
> import org.openrndr.application
> import org.openrndr.color.ColorRGBa
> import org.openrndr.extra.shapes.adjust.adjustContour
> import org.openrndr.shape.Circle
> import org.openrndr.shape.ShapeContour
> 
> ```

```kotlin
fun main() {
    application {
        program {
            val cir = adjustContour(Circle(drawer.bounds.center, 200.0).contour.resample(12)) {
                selectVertices { i, _ -> i % 2 == 0 }
                vertices.forEach {
                    it.moveBy(it.normal * 20.0, true)
                }
            }
            extend {
                drawer.clear(ColorRGBa.WHITE)
                drawer.stroke = ColorRGBa.BLACK.opacify(0.7)
                drawer.fill = ColorRGBa.PINK
                drawer.contour(cir)
            }
        }
    }
}

```

For this to work I wrote a `resample()` function which you can find below.  
What the code does is this: select all the even vertices and displace them following the curve normal at that point by 20 pixels. The `true` argument is the default which makes it update the control points next to each vertex.

By default a Circle only has 4 segments. I wanted to have a circle with more segments so I wrote the following:

```kotlin
/**
 * Returns a modified ShapeContour in which segments have been split into [subdivisions] parts.
 * The new contour should be visually identical to the original.
 */
fun ShapeContour.resample(subdivisions: Int): ShapeContour {
    require(subdivisions >= 2) { "subdivisions must be >= 2 but is actually $subdivisions" }
    return ShapeContour(
        segments.map { seg ->
            List(subdivisions) {
                val t = it / subdivisions.toDouble()
                seg.sub(t, t + 1.0 / subdivisions)
            }
        }.flatten(), closed
    )
}

```

Now we can play a bit. If you toggle the `true` argument (updateTangents) in `moveBy` to `false` the vertices are displaced but the control points are stuck. It looks like this:

![Adjust-2024-05-26-13.19.15](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/bc842cade7440b83c67f8da073cc022013243034.png)

Same, but using `-20.0` instead of `20.0`:

![Adjust-2024-05-26-13.21.36](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/b8e3ce8ec446386a8c1029a64dd2ed566054d1d0.png)

Or shifting 2 out of every 8 vertices inwards:

```kotlin
            val cir = adjustContour(Circle(drawer.bounds.center, 200.0).contour.resample(10)) {
                selectVertices { i, _ -> i % 8 < 2 }
                vertices.forEach {
                    it.moveBy(it.normal * -50.0, true)
                }
            }

```

![Adjust-2024-05-26-13.25.45](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/6153ad6a6c5374ac17410cc1391ea40bf1fcbddf.png)

Or just go a bit crazy 😄

![Adjust-2024-05-26-13.36.09](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/26dbac98bbbaf6b46233bbd72dc7c988230ddb7a.png)

---

<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:** [June 4, 2024, 2:24pm UTC](https://openrndr.discourse.group/t/modifying-contours-with-contouradjuster/645/3 "2024-06-04T14:24:28Z")

</div>

## Adjusting edges

In the examples above we altered the _vertices_ of contours, but it is also possible to alter the _edges_.

In the following example we rotate all the edges of a contour by a varying amount:

 ![AdjustBW2-2024-06-04-15.50.19](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/320e217b0e429b9015cae1222b69e4c789028883.png)

> **imports**
>
> ```kotlin
> import org.openrndr.application
> import org.openrndr.color.ColorRGBa
> import org.openrndr.extensions.Screenshots
> import org.openrndr.extra.shapes.adjust.adjustContour
> import org.openrndr.extra.shapes.hobbycurve.hobbyCurve
> import org.openrndr.shape.Circle
> 
> ```

```kotlin
fun main() {
    application {
        program {
            val cirs = List(10) { n ->
                val c = Circle(drawer.bounds.center, 150.0 + 5.0 * n).contour
                adjustContour(c) {
                    edges.forEach {
                        it.rotate(2.0 * n - 10.0)
                    }
                }
            }
            extend(Screenshots())
            extend {
                drawer.clear(ColorRGBa.WHITE)
                drawer.stroke = ColorRGBa.BLACK
                drawer.fill = null
                drawer.contours(cirs)
            }
        }
    }
}

```

We could also only alter every other edge:

 ![AdjustBW2-2024-06-04-15.51.45](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/4a6292c3fa3df31f57bcb040faff6999264a1c41.png)

```kotlin
val cirs = List(10) { n ->
    val c = Circle(drawer.bounds.center, 150.0 + 5.0 * n).contour
    adjustContour(c) {
        selectEdges { i, e -> i % 2 == 0 }
        edges.forEach {
            it.rotate(2.0 * n - 10.0)
        }
    }
}

```

Lets resample the circle to have 60 segments instead of 4:

 ![AdjustBW2-2024-06-04-15.54.12](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/5af182453b17474c35224a2bd1a9d2c62fa90f03.png)

```kotlin
val cirs = List(10) { n ->
    val c = Circle(drawer.bounds.center, 150.0 + 5.0 * n).contour.sampleEquidistant(60).hobbyCurve()
    adjustContour(c) {
        selectEdges { i, e -> i % 2 == 0 }
        edges.forEach {
            it.rotate(2.0 * n - 10.0)
        }
    }
}

```

Lets make it more obvious what adjusting the edges do. We will create a square centered on the screen, then resample it to have 48 vertices (12 per side) and finally shift one from every four segments outwards:

 ![AdjustBW2-2024-06-04-16.06.16](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/fd32ba6ca94c705aa61b40bd3d6bd92d915ad768.png)

```kotlin
val c = Rectangle.fromCenter(drawer.bounds.center, 200.0).contour.sampleEquidistant(48)
val c2 = adjustContour(c) {
    selectEdges { i, e -> i % 4 == 1 }
    edges.forEach {
        it.moveBy(it.normal(0.5) * 20.0)
    }
}
extend(Screenshots())
extend {
    drawer.clear(ColorRGBa.WHITE)
    drawer.stroke = ColorRGBa.BLACK
    drawer.fill = null
    drawer.contour(c2)
}

```

By scaling the edges up we can avoid the diagonal lines:

 ![AdjustBW2-2024-06-04-16.19.31](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/8e478100dfb0eaab1a3c68f88487903d4f19bcf2.png)

```kotlin
val c = Rectangle.fromCenter(drawer.bounds.center, 200.0).contour.sampleEquidistant(48)
val c2 = adjustContour(c) {
    selectEdges { i, e -> i % 4 == 2 }
    edges.forEach {
        it.moveBy(it.normal(0.5) * 20.0)
        it.scale(3.0)
    }
}

```

And let’s round it a bit so no one gets hurt with those sharp corners:

 ![AdjustBW2-2024-06-04-16.21.09](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/0392baddcef6241c08bb2be42e7f299f30271ea0.png)

```kotlin
val c = Rectangle.fromCenter(drawer.bounds.center, 200.0).contour.sampleEquidistant(48)
val c2 = adjustContour(c) {
    selectEdges { i, e -> i % 4 == 2 }
    edges.forEach {
        it.moveBy(it.normal(0.5) * 20.0)
        it.scale(3.0)
    }
}.roundCorners(8.0)

```

That’s it for now 🙂

---

<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:** [June 4, 2024, 2:53pm UTC](https://openrndr.discourse.group/t/modifying-contours-with-contouradjuster/645/4 "2024-06-04T14:53:26Z")

</div>

Here a design making use of edge shifting and scaling, and creating shapes with pairs of contours. Note that when constructing a shape, the even elements should be reversed to act as holes of the previous ones.

 ![AdjustBW2-2024-06-04-16.50.00](https://global.discourse-cdn.com/free1/uploads/openrndr/original/1X/aabcb533aad04e486a97d107ec99f92315d0e814.png)

> **imports**
>
> ```kotlin
> import org.openrndr.application
> import org.openrndr.color.ColorRGBa
> import org.openrndr.extensions.Screenshots
> import org.openrndr.extra.color.presets.ROSY_BROWN
> import org.openrndr.extra.noise.Random
> import org.openrndr.extra.shapes.adjust.adjustContour
> import org.openrndr.extra.shapes.operators.roundCorners
> import org.openrndr.shape.Shape
> 
> ```

```kotlin
fun main() {
    application {
        configure {
            width = 800
            height = 800
        }
        program {
            val cs = List(8) {
                val c = drawer.bounds.offsetEdges(-30.0 * (it + 4))
                    .contour.sampleEquidistant(4 * Random.int(10, 30))
                val which = Random.int0(4)
                adjustContour(c) {
                    selectEdges { i, _ -> i % 4 == which && Random.bool() }
                    edges.forEach { edge ->
                        edge.moveBy(edge.normal(0.5) * 20.0)
                        edge.scale(3.0)
                    }
                }.roundCorners(5.0)
            }
            val shapes = cs.chunked(2) {
                Shape(listOf(it[0], it[1].reversed))
            }
            extend {
                drawer.clear(ColorRGBa.WHITE)
                drawer.stroke = null
                drawer.fill = ColorRGBa.ROSY_BROWN
                drawer.shapes(shapes)
            }
        }
    }
}

```
