[orx-shapes] Add BezierPatch
This commit is contained in:
40
orx-shapes/src/demo/kotlin/DemoBezierPatch01.kt
Normal file
40
orx-shapes/src/demo/kotlin/DemoBezierPatch01.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extensions.SingleScreenshot
|
||||||
|
import org.openrndr.extra.shapes.bezierPatch
|
||||||
|
import org.openrndr.shape.LineSegment
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
application {
|
||||||
|
configure {
|
||||||
|
width = 800
|
||||||
|
height = 800
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
if (System.getProperty("takeScreenshot") == "true") {
|
||||||
|
extend(SingleScreenshot()) {
|
||||||
|
this.outputFile = System.getProperty("screenshotPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extend {
|
||||||
|
drawer.clear(ColorRGBa.PINK)
|
||||||
|
val c0 = LineSegment(200.0, 100.0, width-200.0, 100.0).contour.segments.first()
|
||||||
|
val c1 = LineSegment(100.0, 150.0, width-100.0, 150.0).contour.segments.first()
|
||||||
|
val c2 = LineSegment(100.0, height-150.0, width-100.0, height-150.0).contour.segments.first()
|
||||||
|
val c3 = LineSegment(200.0, height-100.0, width-200.0, height-100.0).contour.segments.first()
|
||||||
|
|
||||||
|
|
||||||
|
val bp = bezierPatch(c0, c1, c2, c3)
|
||||||
|
val bpsub = bp.sub(0.0, 0.0, 0.5, 0.5)
|
||||||
|
for (i in 0..50) {
|
||||||
|
drawer.stroke = ColorRGBa.BLACK
|
||||||
|
drawer.contour(bp.horizontal(i/50.0))
|
||||||
|
drawer.contour(bp.vertical(i/50.0))
|
||||||
|
drawer.stroke = ColorRGBa.RED
|
||||||
|
drawer.contour(bpsub.horizontal(i/50.0))
|
||||||
|
drawer.contour(bpsub.vertical(i/50.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
orx-shapes/src/demo/kotlin/DemoBezierPatch02.kt
Normal file
37
orx-shapes/src/demo/kotlin/DemoBezierPatch02.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extensions.SingleScreenshot
|
||||||
|
import org.openrndr.extra.shapes.bezierPatch
|
||||||
|
import org.openrndr.math.bezier
|
||||||
|
import org.openrndr.shape.Circle
|
||||||
|
import org.openrndr.shape.LineSegment
|
||||||
|
import org.openrndr.shape.Rectangle
|
||||||
|
import org.openrndr.shape.drawComposition
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
application {
|
||||||
|
configure {
|
||||||
|
width = 800
|
||||||
|
height = 800
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
if (System.getProperty("takeScreenshot") == "true") {
|
||||||
|
extend(SingleScreenshot()) {
|
||||||
|
this.outputFile = System.getProperty("screenshotPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extend {
|
||||||
|
drawer.clear(ColorRGBa.PINK)
|
||||||
|
val c = Circle(width/2.0, height/2.0, 350.0).contour
|
||||||
|
val bp = bezierPatch(c)
|
||||||
|
|
||||||
|
for (i in 0..10) {
|
||||||
|
drawer.stroke = ColorRGBa.BLACK
|
||||||
|
drawer.contour(bp.horizontal(i/10.0))
|
||||||
|
drawer.contour(bp.vertical(i/10.0))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
orx-shapes/src/demo/kotlin/DemoBezierPatch03.kt
Normal file
47
orx-shapes/src/demo/kotlin/DemoBezierPatch03.kt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extensions.Screenshots
|
||||||
|
import org.openrndr.extensions.SingleScreenshot
|
||||||
|
import org.openrndr.extra.shapes.bezierPatch
|
||||||
|
import org.openrndr.extra.shapes.distort
|
||||||
|
import org.openrndr.extra.shapes.regularStarRounded
|
||||||
|
import org.openrndr.math.transforms.transform
|
||||||
|
import org.openrndr.shape.Circle
|
||||||
|
fun main() {
|
||||||
|
application {
|
||||||
|
configure {
|
||||||
|
width = 800
|
||||||
|
height = 800
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
if (System.getProperty("takeScreenshot") == "true") {
|
||||||
|
extend(SingleScreenshot()) {
|
||||||
|
this.outputFile = System.getProperty("screenshotPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extend(Screenshots())
|
||||||
|
extend {
|
||||||
|
drawer.clear(ColorRGBa.PINK)
|
||||||
|
val bp = bezierPatch(Circle(width / 2.0, height / 2.0, 350.0).contour)
|
||||||
|
|
||||||
|
for (i in 0..50) {
|
||||||
|
drawer.stroke = ColorRGBa.BLACK
|
||||||
|
drawer.contour(bp.horizontal(i / 50.0))
|
||||||
|
drawer.contour(bp.vertical(i / 50.0))
|
||||||
|
}
|
||||||
|
drawer.fill = ColorRGBa.PINK
|
||||||
|
for (j in 1 until 10) {
|
||||||
|
for (i in 1 until 10) {
|
||||||
|
val r = regularStarRounded(7, 30.0, 40.0, 0.5, 0.5).transform(
|
||||||
|
transform {
|
||||||
|
translate(j * width / 10.0, i * height / 10.0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val dr = bp.distort(r, drawer.bounds)
|
||||||
|
drawer.contour(dr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
orx-shapes/src/main/kotlin/BezierPatch.kt
Normal file
182
orx-shapes/src/main/kotlin/BezierPatch.kt
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package org.openrndr.extra.shapes
|
||||||
|
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.shape.Rectangle
|
||||||
|
import org.openrndr.shape.Segment
|
||||||
|
import org.openrndr.shape.ShapeContour
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class BezierPatch(val points: List<List<Vector2>>) {
|
||||||
|
init {
|
||||||
|
require(points.size == 4 && points.all { it.size == 4 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a transposed version of the bezier path by transposing the [points] matrix
|
||||||
|
*/
|
||||||
|
val transposed
|
||||||
|
get() = BezierPatch(
|
||||||
|
listOf(
|
||||||
|
listOf(points[0][0], points[1][0], points[2][0], points[3][0]),
|
||||||
|
listOf(points[0][1], points[1][1], points[2][1], points[3][1]),
|
||||||
|
listOf(points[0][2], points[1][2], points[2][2], points[3][2]),
|
||||||
|
listOf(points[0][3], points[1][3], points[2][3], points[3][3]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun coeffs(t: Double): DoubleArray {
|
||||||
|
val it = 1.0 - t
|
||||||
|
val it2 = it * it
|
||||||
|
val it3 = it2 * it
|
||||||
|
val t2 = t * t
|
||||||
|
val t3 = t2 * t
|
||||||
|
return doubleArrayOf(it3, 3 * it2 * t, 3 * it * t2, t3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a point on the patch by using its u,v parameterization
|
||||||
|
* @param u a value between 0 and 1
|
||||||
|
* @param v a value between 0 and 1
|
||||||
|
*/
|
||||||
|
fun position(u: Double, v: Double): Vector2 {
|
||||||
|
val csu = coeffs(u)
|
||||||
|
val csv = coeffs(v)
|
||||||
|
var result = Vector2.ZERO
|
||||||
|
for (j in 0 until 4) {
|
||||||
|
for (i in 0 until 4) {
|
||||||
|
result += points[j][i] * csu[i] * csv[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random point on the path
|
||||||
|
* @return a point that is uniformly distributed in uv space
|
||||||
|
*/
|
||||||
|
fun randomPoint(random: Random = Random.Default) = position(random.nextDouble(), random.nextDouble())
|
||||||
|
|
||||||
|
fun horizontal(v: Double): ShapeContour {
|
||||||
|
val cs = coeffs(v)
|
||||||
|
val cps = Array(4) { Vector2.ZERO }
|
||||||
|
for (j in 0 until 4) {
|
||||||
|
for (i in 0 until 4) {
|
||||||
|
cps[j] += points[i][j] * cs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ShapeContour(listOf(Segment(cps[0], cps[1], cps[2], cps[3])), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vertical(u: Double): ShapeContour {
|
||||||
|
val cs = coeffs(u)
|
||||||
|
val cps = Array(4) { Vector2.ZERO }
|
||||||
|
for (j in 0 until 4) {
|
||||||
|
for (i in 0 until 4) {
|
||||||
|
cps[j] += points[j][i] * cs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ShapeContour(listOf(Segment(cps[0], cps[1], cps[2], cps[3])), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a sub-patch based on uv parameterization
|
||||||
|
*/
|
||||||
|
fun sub(u0: Double, v0: Double, u1: Double, v1: Double): BezierPatch {
|
||||||
|
val c0 = Segment(points[0][0], points[0][1], points[0][2], points[0][3]).sub(u0, u1)
|
||||||
|
val c1 = Segment(points[1][0], points[1][1], points[1][2], points[1][3]).sub(u0, u1)
|
||||||
|
val c2 = Segment(points[2][0], points[2][1], points[2][2], points[2][3]).sub(u0, u1)
|
||||||
|
val c3 = Segment(points[3][0], points[3][1], points[3][2], points[3][3]).sub(u0, u1)
|
||||||
|
|
||||||
|
val sub0 = bezierPatch(c0, c1, c2, c3)
|
||||||
|
val d0 = Segment(sub0.points[0][0], sub0.points[1][0], sub0.points[2][0], sub0.points[3][0]).sub(v0, v1)
|
||||||
|
val d1 = Segment(sub0.points[0][1], sub0.points[1][1], sub0.points[2][1], sub0.points[3][1]).sub(v0, v1)
|
||||||
|
val d2 = Segment(sub0.points[0][2], sub0.points[1][2], sub0.points[2][2], sub0.points[3][2]).sub(v0, v1)
|
||||||
|
val d3 = Segment(sub0.points[0][3], sub0.points[1][3], sub0.points[2][3], sub0.points[3][3]).sub(v0, v1)
|
||||||
|
|
||||||
|
return bezierPatch(d0, d1, d2, d3).transposed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cubic bezier patch from 4 segments. The control points of the segments are used in row-wise fashion
|
||||||
|
*/
|
||||||
|
fun bezierPatch(c0: Segment, c1: Segment, c2: Segment, c3: Segment): BezierPatch {
|
||||||
|
val c0c = c0.cubic
|
||||||
|
val c1c = c1.cubic
|
||||||
|
val c2c = c2.cubic
|
||||||
|
val c3c = c3.cubic
|
||||||
|
|
||||||
|
val c0l = listOf(c0c.start, c0c.control[0], c0c.control[1], c0c.end)
|
||||||
|
val c1l = listOf(c1c.start, c1c.control[0], c1c.control[1], c1c.end)
|
||||||
|
val c2l = listOf(c2c.start, c2c.control[0], c2c.control[1], c2c.end)
|
||||||
|
val c3l = listOf(c3c.start, c3c.control[0], c3c.control[1], c3c.end)
|
||||||
|
|
||||||
|
return BezierPatch(listOf(c0l, c1l, c2l, c3l))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a bezier patch from a closed shape contour (with 4 segments).
|
||||||
|
* @param alpha control for linearity, default is `1.0/3.0`
|
||||||
|
*/
|
||||||
|
fun bezierPatch(shapeContour: ShapeContour, alpha: Double = 1.0 / 3.0): BezierPatch {
|
||||||
|
require(shapeContour.segments.size == 4) {
|
||||||
|
"""contour needs exactly 4 segments (has ${shapeContour.segments.size})"""
|
||||||
|
}
|
||||||
|
val c0 = shapeContour.segments[0].cubic
|
||||||
|
val c1 = shapeContour.segments[1].cubic
|
||||||
|
val c2 = shapeContour.segments[2].cubic
|
||||||
|
val c3 = shapeContour.segments[3].cubic
|
||||||
|
|
||||||
|
val fa = 1.0 - alpha
|
||||||
|
val fb = alpha
|
||||||
|
|
||||||
|
val x00 = (c0.control[0] * fa + c2.control[1] * fb + c3.control[1] * fa + c1.control[0] * fb) / 2.0
|
||||||
|
val x01 = (c0.control[1] * fa + c2.control[0] * fb + c3.control[1] * fb + c1.control[0] * fa) / 2.0
|
||||||
|
val x10 = (c0.control[0] * fb + c2.control[1] * fa + c3.control[0] * fa + c1.control[1] * fb) / 2.0
|
||||||
|
val x11 = (c0.control[1] * fb + c2.control[0] * fa + c3.control[0] * fb + c1.control[1] * fa) / 2.0
|
||||||
|
val cps = listOf(
|
||||||
|
listOf(c0.start, c0.control[0], c0.control[1], c0.end),
|
||||||
|
listOf(c3.control[1], x00, x01, c1.control[0]),
|
||||||
|
listOf(c3.control[0], x10, x11, c1.control[1]),
|
||||||
|
listOf(c2.end, c2.control[1], c2.control[0], c2.start),
|
||||||
|
)
|
||||||
|
return BezierPatch(cps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a bezier patch from 4 corners
|
||||||
|
* @param corners a list of corners from which to create the patch
|
||||||
|
* @param alpha control for linearity, default is `1.0/3.0`
|
||||||
|
*/
|
||||||
|
fun bezierPatch(corners: List<Vector2>, alpha: Double = 1.0 / 3.0): BezierPatch {
|
||||||
|
require(corners.size == 4) {
|
||||||
|
"""need exactly 4 corners (got ${corners.size}"""
|
||||||
|
}
|
||||||
|
return bezierPatch(ShapeContour.fromPoints(corners, true), alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distort a shape contour
|
||||||
|
*/
|
||||||
|
fun BezierPatch.distort(shapeContour: ShapeContour, referenceRectangle: Rectangle = shapeContour.bounds): ShapeContour {
|
||||||
|
val distortedSegments = shapeContour.segments.map {
|
||||||
|
val c = it.cubic
|
||||||
|
val e = c.end.map(referenceRectangle)
|
||||||
|
val c0 = c.control[0].map(referenceRectangle)
|
||||||
|
val c1 = c.control[1].map(referenceRectangle)
|
||||||
|
val s = c.start.map(referenceRectangle)
|
||||||
|
|
||||||
|
val ne = position(e.x, e.y)
|
||||||
|
val ns = position(s.x, s.y)
|
||||||
|
val nc0 = position(c0.x, c0.y)
|
||||||
|
val nc1 = position(c1.x, c1.y)
|
||||||
|
Segment(ns, nc0, nc1, ne)
|
||||||
|
}
|
||||||
|
return ShapeContour(distortedSegments, shapeContour.closed, shapeContour.polarity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Vector2.map(rect: Rectangle): Vector2 {
|
||||||
|
val nx = (x - rect.x) / rect.width
|
||||||
|
val ny = (y - rect.y) / rect.height
|
||||||
|
return Vector2(nx, ny)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user