diff --git a/orx-shapes/src/commonMain/kotlin/blend/ContourBlend.kt b/orx-shapes/src/commonMain/kotlin/blend/ContourBlend.kt new file mode 100644 index 00000000..ab5eb0f2 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/blend/ContourBlend.kt @@ -0,0 +1,35 @@ +package org.openrndr.extra.shapes.blend + +import org.openrndr.extra.shapes.rectify.RectifiedContour +import org.openrndr.extra.shapes.rectify.rectified +import org.openrndr.shape.ShapeContour + +/** + * ContourBlend holds two rectified contours with an equal amount of segments + */ +class ContourBlend(val a: RectifiedContour, val b: RectifiedContour) { + fun mix(blendFunction: (Double) -> Double): ShapeContour { + return a.mix(b, blendFunction) + } + + fun mix(blend: Double): ShapeContour { + return a.mix(b) { blend } + } +} + +/** + * Create a [ContourBlend] for contours [a] and [b] + * + * Finding the pose that minimizes the error between [a] and [b] is not part of this function's work. + * + */ +fun ContourBlend(a: ShapeContour, b: ShapeContour): ContourBlend { + val ra = a.rectified() + val rb = b.rectified() + val sa = ra.splitForBlend(rb) + val sb = rb.splitForBlend(ra) + require(sa.contour.segments.size == sb.contour.segments.size) { + "preprocessing for contours failed to produce equal number of segments. ${sa.contour.segments.size}, ${sb.contour.segments.size}" + } + return ContourBlend(sa, sb) +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/blend/RectifiedContourExtensions.kt b/orx-shapes/src/commonMain/kotlin/blend/RectifiedContourExtensions.kt new file mode 100644 index 00000000..283fbd06 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/blend/RectifiedContourExtensions.kt @@ -0,0 +1,30 @@ +package org.openrndr.extra.shapes.blend + +import org.openrndr.extra.shapes.rectify.RectifiedContour +import org.openrndr.extra.shapes.rectify.rectified +import org.openrndr.extra.shapes.utilities.fromContours +import org.openrndr.shape.ShapeContour + +/** + * Split for blending with [other] + */ +fun RectifiedContour.splitForBlend(other: RectifiedContour): RectifiedContour { + val ts = (0 until other.contour.segments.size + 1).map { it.toDouble() / other.contour.segments.size } + val rts = ts.map { other.inverseRectify(it) } + + return ShapeContour.fromContours(splitAt(rts), contour.closed && other.contour.closed).rectified() +} + +fun RectifiedContour.mix(other: RectifiedContour, blendFunction: (Double) -> Double): ShapeContour { + val n = this.contour.segments.size.toDouble() + val segs = (this.contour.segments zip other.contour.segments).mapIndexed { index, it -> + val t0 = inverseRectify(index / n) + val t1 = inverseRectify((index + 1 / 3.0) / n) + val t2 = inverseRectify((index + 2 / 3.0) / n) + val t3 = inverseRectify((index + 1) / n) + + it.first.mix(it.second, blendFunction(t0), blendFunction(t1), blendFunction(t2), blendFunction(t3)) + } + return ShapeContour.fromSegments(segs, contour.closed && other.contour.closed) +} + diff --git a/orx-shapes/src/commonMain/kotlin/blend/SegmentExtensions.kt b/orx-shapes/src/commonMain/kotlin/blend/SegmentExtensions.kt new file mode 100644 index 00000000..bfb68ffc --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/blend/SegmentExtensions.kt @@ -0,0 +1,28 @@ +package org.openrndr.extra.shapes.blend + +import org.openrndr.math.mix +import org.openrndr.shape.Segment + +/** + * Cubic segment mix + * @param other the segment to mix with + * @param f0 the mix factor for the start point + * @param f1 the mix factor for the first control point + * @param f2 the mix factor for the second control point + * @param f3 the mix factor for the end point + */ +fun Segment.mix(other: Segment, f0: Double, f1: Double, f2: Double, f3: Double): Segment { + val ac = this.cubic + val bc = other.cubic + + val acc = if (ac.corner) 1.0 else 0.0 + val bcc = if (bc.corner) 1.0 else 0.0 + + return Segment( + ac.start.mix(bc.start, f0), + ac.control[0].mix(bc.control[0], f1), + ac.control[1].mix(bc.control[1], f2), + ac.end.mix(bc.end, f3), + corner = mix(acc, bcc, f0) >= 0.5 + ) +} diff --git a/orx-shapes/src/commonMain/kotlin/blend/ShapeContourExtensions.kt b/orx-shapes/src/commonMain/kotlin/blend/ShapeContourExtensions.kt new file mode 100644 index 00000000..80575eff --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/blend/ShapeContourExtensions.kt @@ -0,0 +1,26 @@ +package org.openrndr.extra.shapes.blend + +import org.openrndr.shape.ShapeContour + +/** + * Mix between two [ShapeContour] instances + * + * @param other other [ShapeContour] to mix with + * @param factor the blend factor between 0.0 and 1.0 + * @see ContourBlend + */ +fun ShapeContour.mix(other: ShapeContour, factor: Double): ShapeContour { + return ContourBlend(this, other).mix(factor) +} + +fun ShapeContour.mix(other: ShapeContour, factor: (Double) -> Double): ShapeContour { + return ContourBlend(this, other).mix(factor) +} + +/** + * Create a [ContourBlend] instance for blending between this and [other] + * @see ContourBlend + */ +fun ShapeContour.blend(other: ShapeContour) : ContourBlend { + return ContourBlend(this, other) +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/blend/DemoContourBlend01.kt b/orx-shapes/src/jvmDemo/kotlin/blend/DemoContourBlend01.kt new file mode 100644 index 00000000..022ad165 --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/blend/DemoContourBlend01.kt @@ -0,0 +1,37 @@ +package blend + +import org.openrndr.application +import org.openrndr.draw.isolated +import org.openrndr.extra.shapes.blend.blend +import org.openrndr.extra.shapes.primitives.grid +import org.openrndr.extra.shapes.primitives.regularStar +import org.openrndr.math.Vector2 +import org.openrndr.shape.Circle +import kotlin.math.PI +import kotlin.math.cos + + +/** + * Demonstration of uniform contour blending + */ +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + val a = Circle(Vector2.ZERO, 90.0).contour + val b = regularStar(5, 30.0, 90.0, center = Vector2.ZERO, phase = 180.0) + val blend = a.blend(b) + extend { + drawer.bounds.grid(3, 3).flatten().forEachIndexed { index, it -> + drawer.isolated { + drawer.translate(it.center) + drawer.contour(blend.mix(cos(index * PI * 2.0 / 9.0 + seconds) * 0.5 + 0.5)) + } + } + } + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/blend/DemoContourBlend02.kt b/orx-shapes/src/jvmDemo/kotlin/blend/DemoContourBlend02.kt new file mode 100644 index 00000000..8a574fb1 --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/blend/DemoContourBlend02.kt @@ -0,0 +1,39 @@ +package blend + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.isolated +import org.openrndr.extra.shapes.blend.blend +import org.openrndr.extra.shapes.primitives.grid +import org.openrndr.extra.shapes.primitives.regularStar +import org.openrndr.math.Vector2 +import org.openrndr.shape.Circle +import kotlin.math.PI +import kotlin.math.cos + +/** + * Demonstration of non-uniform contour blending + */ +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + val a = Circle(Vector2.ZERO, 90.0).contour + val b = regularStar(5, 30.0, 90.0, center = Vector2.ZERO, phase = 180.0) + val blend = a.blend(b) + extend { + drawer.clear(ColorRGBa.WHITE) + drawer.fill = ColorRGBa.BLACK + drawer.bounds.grid(3, 3).flatten().forEachIndexed { index, it -> + drawer.isolated { + drawer.translate(it.center) + drawer.contour(blend.mix { t -> cos(t * PI * 2.0 + index * PI * 2.0 / 9.0 + seconds) * 0.5 + 0.5 }) + } + } + } + } + } +} \ No newline at end of file