[orx-composition, orx-svg] Move Composition and SVG code from OPENRNDR to ORX
This commit is contained in:
5
orx-composition/README.md
Normal file
5
orx-composition/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# orx-composition
|
||||
|
||||
Shape composition library
|
||||
|
||||
This code was previously part of `openrndr-draw`.
|
||||
29
orx-composition/build.gradle.kts
Normal file
29
orx-composition/build.gradle.kts
Normal file
@@ -0,0 +1,29 @@
|
||||
plugins {
|
||||
org.openrndr.extra.convention.`kotlin-multiplatform`
|
||||
// kotlinx-serialization ends up on the classpath through openrndr-math and Gradle doesn't know which
|
||||
// version was used. If openrndr were an included build, we probably wouldn't need to do this.
|
||||
// https://github.com/gradle/gradle/issues/20084
|
||||
id(libs.plugins.kotlin.serialization.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.openrndr.application)
|
||||
implementation(libs.openrndr.draw)
|
||||
implementation(libs.openrndr.filter)
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.kotlin.serialization.core)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val jvmDemo by getting {
|
||||
dependencies {
|
||||
implementation(project(":orx-shapes"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
589
orx-composition/src/commonMain/kotlin/Composition.kt
Normal file
589
orx-composition/src/commonMain/kotlin/Composition.kt
Normal file
@@ -0,0 +1,589 @@
|
||||
package org.openrndr.extra.composition
|
||||
|
||||
import org.openrndr.draw.*
|
||||
import org.openrndr.math.*
|
||||
import org.openrndr.math.transforms.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
import org.openrndr.shape.Shape
|
||||
import org.openrndr.shape.ShapeContour
|
||||
import org.openrndr.shape.bounds
|
||||
import kotlin.math.*
|
||||
import kotlin.reflect.*
|
||||
|
||||
/**
|
||||
* Describes a node in a composition
|
||||
*/
|
||||
sealed class CompositionNode {
|
||||
|
||||
var id: String? = null
|
||||
|
||||
var parent: CompositionNode? = null
|
||||
|
||||
/** This CompositionNode's own style. */
|
||||
var style: Style = Style()
|
||||
|
||||
/**
|
||||
* This CompositionNode's computed style.
|
||||
* Where every style attribute is obtained by
|
||||
* overwriting the Style in the following order:
|
||||
* 1. Default style attributes.
|
||||
* 2. Parent Node's computed style's inheritable attributes.
|
||||
* 3. This Node's own style attributes.
|
||||
*/
|
||||
val effectiveStyle: Style
|
||||
get() = when (val p = parent) {
|
||||
is CompositionNode -> style inherit p.effectiveStyle
|
||||
else -> style
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom attributes to be applied to the Node in addition to the Style attributes.
|
||||
*/
|
||||
var attributes = mutableMapOf<String, String?>()
|
||||
|
||||
/**
|
||||
* a map that stores user data
|
||||
*/
|
||||
val userData = mutableMapOf<String, Any>()
|
||||
|
||||
/**
|
||||
* a [Rectangle] that describes the bounding box of the contents
|
||||
*/
|
||||
abstract val bounds: Rectangle
|
||||
|
||||
val effectiveStroke get() = effectiveStyle.stroke.value
|
||||
val effectiveStrokeOpacity get() = effectiveStyle.strokeOpacity.value
|
||||
val effectiveStrokeWeight get() = effectiveStyle.strokeWeight.value
|
||||
val effectiveMiterLimit get() = effectiveStyle.miterLimit.value
|
||||
val effectiveLineCap get() = effectiveStyle.lineCap.value
|
||||
val effectiveLineJoin get() = effectiveStyle.lineJoin.value
|
||||
val effectiveFill get() = effectiveStyle.fill.value
|
||||
val effectiveFillOpacity get() = effectiveStyle.fillOpacity.value
|
||||
val effectiveDisplay get() = effectiveStyle.display.value
|
||||
val effectiveOpacity get() = effectiveStyle.opacity.value
|
||||
val effectiveVisibility get() = effectiveStyle.visibility.value
|
||||
val effectiveShadeStyle get() = effectiveStyle.shadeStyle.value
|
||||
|
||||
/** Calculates the absolute transformation of the current node. */
|
||||
val effectiveTransform: Matrix44
|
||||
get() = when (val p = parent) {
|
||||
is CompositionNode -> transform * p.effectiveTransform
|
||||
else -> transform
|
||||
}
|
||||
|
||||
var stroke
|
||||
get() = style.stroke.value
|
||||
set(value) {
|
||||
style.stroke = when (value) {
|
||||
null -> Paint.None
|
||||
else -> Paint.RGB(value)
|
||||
}
|
||||
}
|
||||
var strokeOpacity
|
||||
get() = style.strokeOpacity.value
|
||||
set(value) {
|
||||
style.strokeOpacity = Numeric.Rational(value)
|
||||
}
|
||||
var strokeWeight
|
||||
get() = style.strokeWeight.value
|
||||
set(value) {
|
||||
style.strokeWeight = Length.Pixels(value)
|
||||
}
|
||||
var miterLimit
|
||||
get() = style.miterLimit.value
|
||||
set(value) {
|
||||
style.miterLimit = Numeric.Rational(value)
|
||||
}
|
||||
var lineCap
|
||||
get() = style.lineCap.value
|
||||
set(value) {
|
||||
style.lineCap = when (value) {
|
||||
org.openrndr.draw.LineCap.BUTT -> LineCap.Butt
|
||||
org.openrndr.draw.LineCap.ROUND -> LineCap.Round
|
||||
org.openrndr.draw.LineCap.SQUARE -> LineCap.Square
|
||||
}
|
||||
}
|
||||
var lineJoin
|
||||
get() = style.lineJoin.value
|
||||
set(value) {
|
||||
style.lineJoin = when (value) {
|
||||
org.openrndr.draw.LineJoin.BEVEL -> LineJoin.Bevel
|
||||
org.openrndr.draw.LineJoin.MITER -> LineJoin.Miter
|
||||
org.openrndr.draw.LineJoin.ROUND -> LineJoin.Round
|
||||
}
|
||||
}
|
||||
var fill
|
||||
get() = style.fill.value
|
||||
set(value) {
|
||||
style.fill = when (value) {
|
||||
null -> Paint.None
|
||||
else -> Paint.RGB(value)
|
||||
}
|
||||
}
|
||||
var fillOpacity
|
||||
get() = style.fillOpacity.value
|
||||
set(value) {
|
||||
style.fillOpacity = Numeric.Rational(value)
|
||||
}
|
||||
var opacity
|
||||
get() = style.opacity.value
|
||||
set(value) {
|
||||
style.opacity = Numeric.Rational(value)
|
||||
}
|
||||
var shadeStyle
|
||||
get() = style.shadeStyle.value
|
||||
set(value) {
|
||||
style.shadeStyle = Shade.Value(value)
|
||||
}
|
||||
var transform
|
||||
get() = style.transform.value
|
||||
set(value) {
|
||||
style.transform = Transform.Matrix(value)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Deprecate this?
|
||||
operator fun KMutableProperty0<Shade>.setValue(thisRef: Style, property: KProperty<*>, value: ShadeStyle) {
|
||||
this.set(Shade.Value(value))
|
||||
}
|
||||
|
||||
fun transform(node: CompositionNode): Matrix44 =
|
||||
(node.parent?.let { transform(it) } ?: Matrix44.IDENTITY) * node.transform
|
||||
|
||||
/**
|
||||
* a [CompositionNode] that holds a single image [ColorBuffer]
|
||||
*/
|
||||
class ImageNode(var image: ColorBuffer, var x: Double, var y: Double, var width: Double, var height: Double) :
|
||||
CompositionNode() {
|
||||
override val bounds: Rectangle
|
||||
get() = Rectangle(0.0, 0.0, width, height).contour.transform(transform(this)).bounds
|
||||
}
|
||||
|
||||
/**
|
||||
* a [CompositionNode] that holds a single [Shape]
|
||||
*/
|
||||
class ShapeNode(var shape: Shape) : CompositionNode() {
|
||||
override val bounds: Rectangle
|
||||
get() {
|
||||
val t = effectiveTransform
|
||||
return if (t === Matrix44.IDENTITY) {
|
||||
shape.bounds
|
||||
} else {
|
||||
shape.bounds.contour.transform(t).bounds
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* apply transforms of all ancestor nodes and return a new detached org.openrndr.shape.ShapeNode with conflated transform
|
||||
*/
|
||||
fun conflate(): ShapeNode {
|
||||
return ShapeNode(shape).also {
|
||||
it.id = id
|
||||
it.parent = parent
|
||||
it.style = style
|
||||
it.transform = transform(this)
|
||||
it.attributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* apply transforms of all ancestor nodes and return a new detached shape node with identity transform and transformed Shape
|
||||
*/
|
||||
fun flatten(): ShapeNode {
|
||||
return ShapeNode(shape.transform(transform(this))).also {
|
||||
it.id = id
|
||||
it.parent = parent
|
||||
it.style = effectiveStyle
|
||||
it.attributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
fun copy(
|
||||
id: String? = this.id,
|
||||
parent: CompositionNode? = null,
|
||||
style: Style = this.style,
|
||||
attributes: MutableMap<String, String?> = this.attributes,
|
||||
shape: Shape = this.shape
|
||||
): ShapeNode {
|
||||
return ShapeNode(shape).also {
|
||||
it.id = id
|
||||
it.parent = parent
|
||||
it.style = style
|
||||
it.attributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ShapeNode) return false
|
||||
if (shape != other.shape) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return shape.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* the local [Shape] with the [effectiveTransform] applied to it
|
||||
*/
|
||||
val effectiveShape
|
||||
get() = shape.transform(effectiveTransform)
|
||||
}
|
||||
|
||||
/**
|
||||
* a [CompositionNode] that holds a single text
|
||||
*/
|
||||
data class TextNode(var text: String, var contour: ShapeContour?) : CompositionNode() {
|
||||
// TODO: This should not be Rectangle.EMPTY
|
||||
override val bounds: Rectangle
|
||||
get() = Rectangle.EMPTY
|
||||
}
|
||||
|
||||
/**
|
||||
* A [CompositionNode] that functions as a group node
|
||||
*/
|
||||
open class GroupNode(open val children: MutableList<CompositionNode> = mutableListOf()) : CompositionNode() {
|
||||
override val bounds: Rectangle
|
||||
get() {
|
||||
return children.map { it.bounds }.bounds
|
||||
}
|
||||
|
||||
fun copy(
|
||||
id: String? = this.id,
|
||||
parent: CompositionNode? = null,
|
||||
style: Style = this.style,
|
||||
children: MutableList<CompositionNode> = this.children
|
||||
): GroupNode {
|
||||
return GroupNode(children).also {
|
||||
it.id = id
|
||||
it.parent = parent
|
||||
it.style = style
|
||||
it.attributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is GroupNode) return false
|
||||
|
||||
if (children != other.children) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return children.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
data class CompositionDimensions(val x: Length, val y: Length, val width: Length, val height: Length) {
|
||||
val position = Vector2((x as Length.Pixels).value, (y as Length.Pixels).value)
|
||||
val dimensions = Vector2((width as Length.Pixels).value, (height as Length.Pixels).value)
|
||||
|
||||
constructor(rectangle: Rectangle) : this(
|
||||
rectangle.corner.x.pixels,
|
||||
rectangle.corner.y.pixels,
|
||||
rectangle.dimensions.x.pixels,
|
||||
rectangle.dimensions.y.pixels
|
||||
)
|
||||
|
||||
override fun toString(): String = "$x $y $width $height"
|
||||
|
||||
// I'm not entirely sure why this is needed but
|
||||
// but otherwise equality checks will never succeed
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is CompositionDimensions
|
||||
&& x.value == other.x.value
|
||||
&& y.value == other.y.value
|
||||
&& width.value == other.width.value
|
||||
&& height.value == other.height.value
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = x.hashCode()
|
||||
result = 31 * result + y.hashCode()
|
||||
result = 31 * result + width.hashCode()
|
||||
result = 31 * result + height.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
val defaultCompositionDimensions = CompositionDimensions(0.0.pixels, 0.0.pixels, 768.0.pixels, 576.0.pixels)
|
||||
|
||||
|
||||
class GroupNodeStop(children: MutableList<CompositionNode>) : GroupNode(children)
|
||||
|
||||
/**
|
||||
* A vector composition.
|
||||
* @param root the root node of the composition
|
||||
* @param bounds the dimensions of the composition
|
||||
*/
|
||||
class Composition(val root: CompositionNode, var bounds: CompositionDimensions = defaultCompositionDimensions) {
|
||||
constructor(root: CompositionNode, bounds: Rectangle) : this(root, CompositionDimensions(bounds))
|
||||
|
||||
/** SVG/XML namespaces */
|
||||
val namespaces = mutableMapOf<String, String>()
|
||||
|
||||
var style: Style = Style()
|
||||
|
||||
/**
|
||||
* The style attributes affecting the whole document, such as the viewBox area and aspect ratio.
|
||||
*/
|
||||
var documentStyle: DocumentStyle = DocumentStyle()
|
||||
|
||||
init {
|
||||
val (x, y, width, height) = bounds
|
||||
style.x = x
|
||||
style.y = y
|
||||
style.width = width
|
||||
style.height = height
|
||||
}
|
||||
|
||||
fun findShapes() = root.findShapes()
|
||||
fun findShape(id: String): ShapeNode? {
|
||||
return (root.find { it is ShapeNode && it.id == id }) as? ShapeNode
|
||||
}
|
||||
|
||||
fun findImages() = root.findImages()
|
||||
fun findImage(id: String): ImageNode? {
|
||||
return (root.find { it is ImageNode && it.id == id }) as? ImageNode
|
||||
}
|
||||
|
||||
fun findGroups(): List<GroupNode> = root.findGroups()
|
||||
fun findGroup(id: String): GroupNode? {
|
||||
return (root.find { it is GroupNode && it.id == id }) as? GroupNode
|
||||
}
|
||||
|
||||
fun clear() = (root as? GroupNode)?.children?.clear()
|
||||
|
||||
/** Calculates the equivalent of `1%` in pixels. */
|
||||
internal fun normalizedDiagonalLength(): Double = sqrt(bounds.dimensions.squaredLength / 2.0)
|
||||
|
||||
/**
|
||||
* Calculates effective viewport transformation using [viewBox] and [preserveAspectRatio].
|
||||
* As per [the SVG 2.0 spec](https://svgwg.org/svg2-draft/single-page.html#coords-ComputingAViewportsTransform).
|
||||
*/
|
||||
fun calculateViewportTransform(): Matrix44 {
|
||||
return when (documentStyle.viewBox) {
|
||||
ViewBox.None -> Matrix44.IDENTITY
|
||||
is ViewBox.Value -> {
|
||||
when (val vb = (documentStyle.viewBox as ViewBox.Value).value) {
|
||||
Rectangle.EMPTY -> {
|
||||
// The intent is to not display the element
|
||||
Matrix44.ZERO
|
||||
}
|
||||
|
||||
else -> {
|
||||
val vbCorner = vb.corner
|
||||
val vbDims = vb.dimensions
|
||||
val eCorner = bounds.position
|
||||
val eDims = bounds.dimensions
|
||||
val (align, meetOrSlice) = documentStyle.preserveAspectRatio
|
||||
|
||||
val scale = (eDims / vbDims).let {
|
||||
if (align != Align.NONE) {
|
||||
if (meetOrSlice == MeetOrSlice.MEET) {
|
||||
Vector2(min(it.x, it.y))
|
||||
} else {
|
||||
Vector2(max(it.x, it.y))
|
||||
}
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
val translate = (eCorner - (vbCorner * scale)).let {
|
||||
val cx = eDims.x - vbDims.x * scale.x
|
||||
val cy = eDims.y - vbDims.y * scale.y
|
||||
it + when (align) {
|
||||
// TODO: This first one probably doesn't comply with the spec
|
||||
Align.NONE -> Vector2.ZERO
|
||||
Align.X_MIN_Y_MIN -> Vector2.ZERO
|
||||
Align.X_MID_Y_MIN -> Vector2(cx / 2, 0.0)
|
||||
Align.X_MAX_Y_MIN -> Vector2(cx, 0.0)
|
||||
Align.X_MIN_Y_MID -> Vector2(0.0, cy / 2)
|
||||
Align.X_MID_Y_MID -> Vector2(cx / 2, cy / 2)
|
||||
Align.X_MAX_Y_MID -> Vector2(cx, cy / 2)
|
||||
Align.X_MIN_Y_MAX -> Vector2(0.0, cy)
|
||||
Align.X_MID_Y_MAX -> Vector2(cx / 2, cy)
|
||||
Align.X_MAX_Y_MAX -> Vector2(cx, cy)
|
||||
}
|
||||
}
|
||||
|
||||
buildTransform {
|
||||
translate(translate)
|
||||
scale(scale.x, scale.y, 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* remove node from its parent [CompositionNode]
|
||||
*/
|
||||
fun CompositionNode.remove() {
|
||||
require(parent != null) { "parent is null" }
|
||||
val parentGroup = (parent as? GroupNode)
|
||||
if (parentGroup != null) {
|
||||
val filtered = parentGroup.children.filter {
|
||||
it != this
|
||||
}
|
||||
parentGroup.children.clear()
|
||||
parentGroup.children.addAll(filtered)
|
||||
}
|
||||
parent = null
|
||||
}
|
||||
|
||||
fun CompositionNode.findTerminals(filter: (CompositionNode) -> Boolean): List<CompositionNode> {
|
||||
val result = mutableListOf<CompositionNode>()
|
||||
fun find(node: CompositionNode) {
|
||||
when (node) {
|
||||
is GroupNode -> node.children.forEach { find(it) }
|
||||
else -> if (filter(node)) {
|
||||
result.add(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
find(this)
|
||||
return result
|
||||
}
|
||||
|
||||
fun CompositionNode.findAll(filter: (CompositionNode) -> Boolean): List<CompositionNode> {
|
||||
val result = mutableListOf<CompositionNode>()
|
||||
fun find(node: CompositionNode) {
|
||||
if (filter(node)) {
|
||||
result.add(node)
|
||||
}
|
||||
if (node is GroupNode) {
|
||||
node.children.forEach { find(it) }
|
||||
}
|
||||
}
|
||||
find(this)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds first [CompositionNode] to match the given [predicate].
|
||||
*/
|
||||
fun CompositionNode.find(predicate: (CompositionNode) -> Boolean): CompositionNode? {
|
||||
if (predicate(this)) {
|
||||
return this
|
||||
} else if (this is GroupNode) {
|
||||
val deque: ArrayDeque<CompositionNode> = ArrayDeque(children)
|
||||
while (deque.isNotEmpty()) {
|
||||
val node = deque.removeFirst()
|
||||
if (predicate(node)) {
|
||||
return node
|
||||
} else if (node is GroupNode) {
|
||||
deque.addAll(node.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* find all descendant [ShapeNode] nodes, including potentially this node
|
||||
* @return a [List] of [ShapeNode] nodes
|
||||
*/
|
||||
fun CompositionNode.findShapes(): List<ShapeNode> = findTerminals { it is ShapeNode }.map { it as ShapeNode }
|
||||
|
||||
/**
|
||||
* find all descendant [ImageNode] nodes, including potentially this node
|
||||
* @return a [List] of [ImageNode] nodes
|
||||
*/
|
||||
fun CompositionNode.findImages(): List<ImageNode> = findTerminals { it is ImageNode }.map { it as ImageNode }
|
||||
|
||||
/**
|
||||
* find all descendant [GroupNode] nodes, including potentially this node
|
||||
* @return a [List] of [GroupNode] nodes
|
||||
*/
|
||||
fun CompositionNode.findGroups(): List<GroupNode> = findAll { it is GroupNode }.map { it as GroupNode }
|
||||
|
||||
/**
|
||||
* visit this [CompositionNode] and all descendant nodes and execute [visitor]
|
||||
*/
|
||||
fun CompositionNode.visitAll(visitor: (CompositionNode.() -> Unit)) {
|
||||
visitor()
|
||||
if (this is GroupNode) {
|
||||
for (child in children) {
|
||||
child.visitAll(visitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* org.openrndr.shape.UserData delegate
|
||||
*/
|
||||
class UserData<T : Any>(
|
||||
val name: String, val initial: T
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
operator fun getValue(node: CompositionNode, property: KProperty<*>): T {
|
||||
val value: T? = node.userData[name] as? T
|
||||
return value ?: initial
|
||||
}
|
||||
|
||||
operator fun setValue(stylesheet: CompositionNode, property: KProperty<*>, value: T) {
|
||||
stylesheet.userData[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun CompositionNode.filter(filter: (CompositionNode) -> Boolean): CompositionNode? {
|
||||
val f = filter(this)
|
||||
|
||||
if (!f) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this is GroupNode) {
|
||||
val copies = mutableListOf<CompositionNode>()
|
||||
children.forEach {
|
||||
val filtered = it.filter(filter)
|
||||
if (filtered != null) {
|
||||
when (filtered) {
|
||||
is ShapeNode -> {
|
||||
copies.add(filtered.copy(parent = this))
|
||||
}
|
||||
|
||||
is GroupNode -> {
|
||||
copies.add(filtered.copy(parent = this))
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return GroupNode(children = copies)
|
||||
} else {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
fun CompositionNode.map(mapper: (CompositionNode) -> CompositionNode): CompositionNode {
|
||||
val r = mapper(this)
|
||||
return when (r) {
|
||||
is GroupNodeStop -> {
|
||||
r.copy().also { copy ->
|
||||
copy.children.forEach {
|
||||
it.parent = copy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is GroupNode -> {
|
||||
val copy = r.copy(children = r.children.map { it.map(mapper) }.toMutableList())
|
||||
copy.children.forEach {
|
||||
it.parent = copy
|
||||
}
|
||||
copy
|
||||
}
|
||||
|
||||
else -> r
|
||||
}
|
||||
}
|
||||
760
orx-composition/src/commonMain/kotlin/CompositionDrawer.kt
Normal file
760
orx-composition/src/commonMain/kotlin/CompositionDrawer.kt
Normal file
@@ -0,0 +1,760 @@
|
||||
package org.openrndr.extra.composition
|
||||
|
||||
import org.openrndr.collections.pflatMap
|
||||
import org.openrndr.collections.pforEach
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.ColorBuffer
|
||||
import org.openrndr.draw.LineCap
|
||||
import org.openrndr.draw.LineJoin
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.YPolarity
|
||||
import org.openrndr.math.transforms.*
|
||||
import org.openrndr.shape.*
|
||||
|
||||
/**
|
||||
* Used internally to define [ClipMode]s.
|
||||
*/
|
||||
enum class ClipOp {
|
||||
DISABLED,
|
||||
DIFFERENCE,
|
||||
REVERSE_DIFFERENCE,
|
||||
INTERSECT,
|
||||
UNION
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies if transformations should be kept separate
|
||||
* or applied to the clipped object and reset to identity.
|
||||
*/
|
||||
enum class TransformMode {
|
||||
KEEP,
|
||||
APPLY
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies in which way to combine [Shape]s
|
||||
* to form a [Composition]
|
||||
*/
|
||||
enum class ClipMode(val grouped: Boolean, val op: ClipOp) {
|
||||
DISABLED(false, ClipOp.DISABLED),
|
||||
DIFFERENCE(false, ClipOp.DIFFERENCE),
|
||||
DIFFERENCE_GROUP(true, ClipOp.DIFFERENCE),
|
||||
REVERSE_DIFFERENCE(false, ClipOp.REVERSE_DIFFERENCE),
|
||||
REVERSE_DIFFERENCE_GROUP(true, ClipOp.REVERSE_DIFFERENCE),
|
||||
INTERSECT(false, ClipOp.INTERSECT),
|
||||
INTERSECT_GROUP(true, ClipOp.INTERSECT),
|
||||
UNION(false, ClipOp.UNION),
|
||||
UNION_GROUP(true, ClipOp.UNION)
|
||||
}
|
||||
|
||||
/**
|
||||
* The set of draw style properties used for rendering a [Composition]
|
||||
*/
|
||||
private data class CompositionDrawStyle(
|
||||
var fill: ColorRGBa? = null,
|
||||
var fillOpacity: Double = 1.0,
|
||||
var stroke: ColorRGBa? = ColorRGBa.BLACK,
|
||||
var strokeOpacity: Double = 1.0,
|
||||
var strokeWeight: Double = 1.0,
|
||||
var opacity: Double = 1.0,
|
||||
var clipMode: ClipMode = ClipMode.DISABLED,
|
||||
var mask: Shape? = null,
|
||||
var transformMode: TransformMode = TransformMode.APPLY,
|
||||
var lineCap: LineCap = LineCap.BUTT,
|
||||
var lineJoin: LineJoin = LineJoin.MITER,
|
||||
var miterlimit: Double = 4.0,
|
||||
var visibility: Visibility = Visibility.Visible
|
||||
)
|
||||
|
||||
/**
|
||||
* Data structure containing intersection information.
|
||||
*/
|
||||
data class ShapeNodeIntersection(val node: ShapeNode, val intersection: ContourIntersection)
|
||||
|
||||
/**
|
||||
* Data structure containing information about a point
|
||||
* in a [ShapeContour] closest to some other 2D point.
|
||||
*/
|
||||
data class ShapeNodeNearestContour(val node: ShapeNode, val point: ContourPoint, val distanceDirection: Vector2, val distance: Double)
|
||||
|
||||
/**
|
||||
* Merges two lists of [ShapeNodeIntersection] removing duplicates under the
|
||||
* given [threshold]. Used internally by [intersections].
|
||||
*/
|
||||
fun List<ShapeNodeIntersection>.merge(threshold: Double = 0.5): List<ShapeNodeIntersection> {
|
||||
val result = mutableListOf<ShapeNodeIntersection>()
|
||||
for (i in this) {
|
||||
val nearest = result.minByOrNull { it.intersection.position.squaredDistanceTo(i.intersection.position) }
|
||||
if (nearest == null) {
|
||||
result.add(i)
|
||||
} else if (nearest.intersection.position.squaredDistanceTo(i.intersection.position) >= threshold * threshold) {
|
||||
result.add(i)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A Drawer-like interface for the creation of Compositions
|
||||
* This should be easier than creating Compositions manually
|
||||
*/
|
||||
class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositionDimensions,
|
||||
composition: Composition? = null,
|
||||
cursor: GroupNode? = composition?.root as? GroupNode
|
||||
) {
|
||||
val root = (composition?.root as? GroupNode) ?: GroupNode()
|
||||
val composition = composition ?: Composition(root, documentBounds)
|
||||
|
||||
var cursor = cursor ?: root
|
||||
private set
|
||||
|
||||
private val modelStack = ArrayDeque<Matrix44>()
|
||||
private val styleStack = ArrayDeque<CompositionDrawStyle>().apply { }
|
||||
private var drawStyle = CompositionDrawStyle()
|
||||
|
||||
var model = Matrix44.IDENTITY
|
||||
|
||||
var fill
|
||||
get() = drawStyle.fill?.opacify(drawStyle.fillOpacity)?.opacify(drawStyle.opacity)
|
||||
set(value) = run {
|
||||
drawStyle.fill = value?.copy(alpha = 1.0)
|
||||
drawStyle.fillOpacity = value?.alpha ?: 1.0
|
||||
}
|
||||
|
||||
var fillOpacity
|
||||
get() = drawStyle.fillOpacity
|
||||
set(value) = run { drawStyle.fillOpacity = value }
|
||||
|
||||
var stroke
|
||||
get() = drawStyle.stroke?.opacify(drawStyle.strokeOpacity)?.opacify(drawStyle.opacity)
|
||||
set(value) = run {
|
||||
drawStyle.stroke = value?.copy(alpha = 1.0)
|
||||
drawStyle.strokeOpacity = value?.alpha ?: 1.0
|
||||
}
|
||||
|
||||
var strokeOpacity
|
||||
get() = drawStyle.strokeOpacity
|
||||
set(value) = run { drawStyle.strokeOpacity = value }
|
||||
|
||||
var strokeWeight
|
||||
get() = drawStyle.strokeWeight
|
||||
set(value) = run { drawStyle.strokeWeight = value }
|
||||
|
||||
var miterlimit
|
||||
get() = drawStyle.miterlimit
|
||||
set(value) = run { drawStyle.miterlimit = value }
|
||||
|
||||
var lineCap
|
||||
get() = drawStyle.lineCap
|
||||
set(value) = run { drawStyle.lineCap = value }
|
||||
|
||||
var lineJoin
|
||||
get() = drawStyle.lineJoin
|
||||
set(value) = run { drawStyle.lineJoin = value }
|
||||
|
||||
var opacity
|
||||
get() = drawStyle.opacity
|
||||
set(value) = run { drawStyle.opacity = value }
|
||||
|
||||
var visibility
|
||||
get() = drawStyle.visibility
|
||||
set(value) = run { drawStyle.visibility = value }
|
||||
|
||||
var clipMode
|
||||
get() = drawStyle.clipMode
|
||||
set(value) = run { drawStyle.clipMode = value }
|
||||
|
||||
var mask: Shape?
|
||||
get() = drawStyle.mask
|
||||
set(value) = run { drawStyle.mask = value }
|
||||
|
||||
var transformMode
|
||||
get() = drawStyle.transformMode
|
||||
set(value) = run { drawStyle.transformMode = value }
|
||||
|
||||
fun pushModel() {
|
||||
modelStack.addLast(model)
|
||||
}
|
||||
|
||||
fun popModel() {
|
||||
model = modelStack.removeLast()
|
||||
}
|
||||
|
||||
fun pushStyle() {
|
||||
styleStack.addLast(drawStyle.copy())
|
||||
}
|
||||
|
||||
fun popStyle() {
|
||||
drawStyle = styleStack.removeLast()
|
||||
}
|
||||
|
||||
fun isolated(draw: CompositionDrawer.() -> Unit) {
|
||||
pushModel()
|
||||
pushStyle()
|
||||
draw()
|
||||
popModel()
|
||||
popStyle()
|
||||
}
|
||||
|
||||
fun GroupNode.with(builder: CompositionDrawer.() -> Unit): GroupNode {
|
||||
val oldCursor = cursor
|
||||
cursor = this
|
||||
builder()
|
||||
cursor = oldCursor
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group node and run `builder` inside its context
|
||||
* @param insert if true the created group will be inserted at [cursor]
|
||||
* @param id an optional identifier
|
||||
* @param builder the function that is executed inside the group context
|
||||
*/
|
||||
fun group(insert: Boolean = true, id: String? = null, builder: CompositionDrawer.() -> Unit): GroupNode {
|
||||
val group = GroupNode()
|
||||
group.id = id
|
||||
val oldCursor = cursor
|
||||
|
||||
if (insert) {
|
||||
cursor.children.add(group)
|
||||
group.parent = cursor
|
||||
}
|
||||
cursor = group
|
||||
builder()
|
||||
|
||||
cursor = oldCursor
|
||||
return group
|
||||
}
|
||||
|
||||
fun translate(x: Double, y: Double) = translate(Vector2(x, y))
|
||||
|
||||
fun rotate(rotationInDegrees: Double) {
|
||||
model *= Matrix44.rotateZ(rotationInDegrees)
|
||||
}
|
||||
|
||||
fun scale(s: Double) {
|
||||
model *= Matrix44.scale(s, s, s)
|
||||
}
|
||||
|
||||
fun scale(x: Double, y: Double) {
|
||||
model *= Matrix44.scale(x, y, 1.0)
|
||||
}
|
||||
|
||||
fun translate(t: Vector2) {
|
||||
model *= Matrix44.translate(t.vector3())
|
||||
}
|
||||
|
||||
fun contour(contour: ShapeContour, insert: Boolean = true): ShapeNode? {
|
||||
if (contour.empty) {
|
||||
return null
|
||||
}
|
||||
val shape = Shape(listOf(contour))
|
||||
return shape(shape, insert)
|
||||
}
|
||||
|
||||
fun contours(contours: List<ShapeContour>, insert: Boolean = true) = contours.map { contour(it, insert) }
|
||||
|
||||
/**
|
||||
* Search for a point on a contour in the composition tree that's nearest to `point`
|
||||
* @param point the query point
|
||||
* @param searchFrom a node from which the search starts, defaults to composition root
|
||||
* @return an optional org.openrndr.shape.ShapeNodeNearestContour instance
|
||||
*/
|
||||
fun nearest(
|
||||
point: Vector2,
|
||||
searchFrom: CompositionNode = composition.root as GroupNode
|
||||
): ShapeNodeNearestContour? {
|
||||
return distances(point, searchFrom).firstOrNull()
|
||||
}
|
||||
|
||||
fun CompositionNode.nearest(point: Vector2) = nearest(point, searchFrom = this)
|
||||
|
||||
fun difference(
|
||||
shape: Shape,
|
||||
searchFrom: CompositionNode = composition.root as GroupNode
|
||||
): Shape {
|
||||
val shapes = searchFrom.findShapes()
|
||||
var from = shape
|
||||
|
||||
for (subtract in shapes) {
|
||||
if (shape.bounds.intersects(subtract.shape.bounds)) {
|
||||
from = difference(from, subtract.shape)
|
||||
}
|
||||
}
|
||||
return from
|
||||
}
|
||||
|
||||
/**
|
||||
* Find distances to each contour in the composition tree (or starting node)
|
||||
* @param point the query point
|
||||
* @param searchFrom a node from which the search starts, defaults to composition root
|
||||
* @return a sorted list of [ShapeNodeNearestContour] describing distance to every contour
|
||||
*/
|
||||
fun distances(
|
||||
point: Vector2,
|
||||
searchFrom: CompositionNode = composition.root as GroupNode
|
||||
): List<ShapeNodeNearestContour> {
|
||||
return searchFrom.findShapes().flatMap { node ->
|
||||
node.shape.contours.filter { !it.empty }
|
||||
.map { it.nearest(point) }
|
||||
.map { ShapeNodeNearestContour(node, it, point - it.position, it.position.distanceTo(point)) }
|
||||
}.sortedBy { it.distance }
|
||||
}
|
||||
|
||||
fun CompositionNode.distances(point: Vector2): List<ShapeNodeNearestContour> = distances(point, searchFrom = this)
|
||||
|
||||
/**
|
||||
* Test a given `contour` against org.openrndr.shape.contours in the composition tree
|
||||
* @param contour the query contour
|
||||
* @param searchFrom a node from which the search starts, defaults to composition root
|
||||
* @param mergeThreshold minimum distance between intersections before they are merged together,
|
||||
* 0.0 or lower means no org.openrndr.shape.merge
|
||||
* @return a list of `org.openrndr.shape.ShapeNodeIntersection`
|
||||
*/
|
||||
fun intersections(
|
||||
contour: ShapeContour,
|
||||
searchFrom: CompositionNode = composition.root as GroupNode,
|
||||
mergeThreshold: Double = 0.5
|
||||
): List<ShapeNodeIntersection> {
|
||||
return searchFrom.findShapes().pflatMap { node ->
|
||||
if (node.bounds.intersects(contour.bounds)) {
|
||||
node.shape.contours.flatMap { nodeContour ->
|
||||
intersections(contour, nodeContour).map {
|
||||
ShapeNodeIntersection(node, it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}.let {
|
||||
if (mergeThreshold > 0.0) {
|
||||
it.merge(mergeThreshold)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun CompositionNode.intersections(contour: ShapeContour, mergeThreshold: Double = 0.5) =
|
||||
intersections(contour, this, mergeThreshold)
|
||||
|
||||
/**
|
||||
* Test a given `shape` against org.openrndr.shape.contours in the composition tree
|
||||
* @param shape the query shape
|
||||
* @param searchFrom a node from which the search starts, defaults to composition root
|
||||
* @return a list of `org.openrndr.shape.ShapeNodeIntersection`
|
||||
*/
|
||||
fun intersections(
|
||||
shape: Shape,
|
||||
searchFrom: CompositionNode = composition.root as GroupNode,
|
||||
mergeThreshold: Double = 0.5
|
||||
): List<ShapeNodeIntersection> {
|
||||
return shape.contours.flatMap {
|
||||
intersections(it, searchFrom, mergeThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
fun CompositionNode.intersections(shape: Shape, mergeThreshold: Double = 0.5) =
|
||||
intersections(shape, this, mergeThreshold)
|
||||
|
||||
|
||||
fun shape(shape: Shape, insert: Boolean = true): ShapeNode? {
|
||||
if (shape.empty) {
|
||||
return null
|
||||
}
|
||||
|
||||
val inverseModel = model.inversed
|
||||
val postShape = mask?.let { intersection(shape, it.transform(inverseModel)) } ?: shape
|
||||
|
||||
if (postShape.empty) {
|
||||
return null
|
||||
}
|
||||
|
||||
// only use clipping for open shapes
|
||||
val clipMode = if (postShape.topology == ShapeTopology.CLOSED) clipMode else ClipMode.DISABLED
|
||||
|
||||
return when (clipMode.op) {
|
||||
ClipOp.DISABLED, ClipOp.REVERSE_DIFFERENCE -> {
|
||||
val shapeNode = ShapeNode(postShape)
|
||||
|
||||
val shapeTransform: Matrix44 = when (transformMode) {
|
||||
TransformMode.KEEP -> {
|
||||
shapeNode.transform = model
|
||||
Matrix44.IDENTITY
|
||||
}
|
||||
TransformMode.APPLY -> {
|
||||
shapeNode.transform = Matrix44.IDENTITY
|
||||
model
|
||||
}
|
||||
}
|
||||
shapeNode.shape = when (clipMode.op) {
|
||||
ClipOp.DISABLED -> postShape.transform(shapeTransform)
|
||||
ClipOp.REVERSE_DIFFERENCE -> {
|
||||
val shapeNodes = (if (!clipMode.grouped) composition.findShapes() else cursor.findShapes())
|
||||
var toInsert = shape
|
||||
val inverse = model.inversed
|
||||
for (node in shapeNodes) {
|
||||
if (toInsert.empty) {
|
||||
break
|
||||
} else {
|
||||
toInsert = difference(toInsert, node.effectiveShape.transform(inverse))
|
||||
}
|
||||
}
|
||||
toInsert
|
||||
}
|
||||
else -> error("unreachable")
|
||||
}
|
||||
shapeNode.stroke = stroke
|
||||
shapeNode.strokeOpacity = strokeOpacity
|
||||
shapeNode.strokeWeight = strokeWeight
|
||||
shapeNode.miterLimit = miterlimit
|
||||
shapeNode.lineCap = lineCap
|
||||
shapeNode.lineJoin = lineJoin
|
||||
shapeNode.fill = fill
|
||||
shapeNode.fillOpacity = fillOpacity
|
||||
|
||||
if (insert) {
|
||||
cursor.children.add(shapeNode)
|
||||
shapeNode.parent = cursor
|
||||
}
|
||||
shapeNode
|
||||
}
|
||||
else -> {
|
||||
val shapeNodes = (if (!clipMode.grouped) composition.findShapes() else cursor.findShapes())
|
||||
val toRemove = mutableListOf<CompositionNode>()
|
||||
shapeNodes.pforEach { shapeNode ->
|
||||
val inverse = shapeNode.effectiveTransform.inversed
|
||||
val transformedShape = postShape.transform(inverse * model)
|
||||
val operated =
|
||||
when (clipMode.op) {
|
||||
ClipOp.INTERSECT -> intersection(shapeNode.shape, transformedShape)
|
||||
ClipOp.UNION -> union(shapeNode.shape, transformedShape)
|
||||
ClipOp.DIFFERENCE -> difference(shapeNode.shape, transformedShape)
|
||||
else -> error("unsupported base op ${clipMode.op}")
|
||||
}
|
||||
if (!operated.empty) {
|
||||
shapeNode.shape = operated
|
||||
} else {
|
||||
//synchronized(toRemove) {
|
||||
toRemove.add(shapeNode)
|
||||
//}
|
||||
}
|
||||
}
|
||||
for (node in toRemove) {
|
||||
node.remove()
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shapes(shapes: List<Shape>, insert: Boolean = true) = shapes.map { shape(it, insert) }
|
||||
|
||||
fun rectangle(rectangle: Rectangle, closed: Boolean = true, insert: Boolean = true) = contour(rectangle.contour.let {
|
||||
if (closed) {
|
||||
it
|
||||
} else {
|
||||
it.open
|
||||
}
|
||||
}, insert = insert)
|
||||
|
||||
fun rectangle(x: Double, y: Double, width: Double, height: Double, closed: Boolean = true, insert: Boolean = true) = rectangle(
|
||||
Rectangle(x, y, width, height), closed, insert)
|
||||
|
||||
fun rectangles(rectangles: List<Rectangle>, insert: Boolean = true) = rectangles.map { rectangle(it, insert) }
|
||||
|
||||
fun rectangles(positions: List<Vector2>, width: Double, height: Double, insert: Boolean = true) = rectangles(positions.map {
|
||||
Rectangle(it, width, height)
|
||||
}, insert)
|
||||
|
||||
fun rectangles(positions: List<Vector2>, dimensions: List<Vector2>, insert: Boolean) = rectangles((positions zip dimensions).map {
|
||||
Rectangle(it.first, it.second.x, it.second.y)
|
||||
}, insert)
|
||||
|
||||
fun circle(x: Double, y: Double, radius: Double, closed: Boolean = true, insert: Boolean = true) = circle(
|
||||
Circle(
|
||||
Vector2(x, y),
|
||||
radius
|
||||
), closed, insert)
|
||||
|
||||
fun circle(position: Vector2, radius: Double, closed: Boolean = true, insert: Boolean = true) = circle(
|
||||
Circle(
|
||||
position,
|
||||
radius
|
||||
), closed, insert)
|
||||
|
||||
fun circle(circle: Circle, closed: Boolean = true, insert: Boolean = true) = contour(circle.contour.let {
|
||||
if (closed) {
|
||||
it
|
||||
} else {
|
||||
it.open
|
||||
}
|
||||
}, insert)
|
||||
|
||||
fun circles(circles: List<Circle>, insert: Boolean = true) = circles.map { circle(it, insert) }
|
||||
|
||||
fun circles(positions: List<Vector2>, radius: Double, insert: Boolean = true) = circles(positions.map {
|
||||
Circle(
|
||||
it,
|
||||
radius
|
||||
)
|
||||
}, insert)
|
||||
|
||||
fun circles(positions: List<Vector2>, radii: List<Double>, insert: Boolean = true) = circles((positions zip radii).map {
|
||||
Circle(
|
||||
it.first,
|
||||
it.second
|
||||
)
|
||||
}, insert)
|
||||
|
||||
/*
|
||||
fun ellipse(
|
||||
x: Double,
|
||||
y: Double,
|
||||
xRadius: Double,
|
||||
yRadius: Double,
|
||||
rotationInDegrees: Double = 0.0,
|
||||
closed: Boolean = true,
|
||||
insert: Boolean = true
|
||||
) = ellipse(Vector2(x, y), xRadius, yRadius, rotationInDegrees, closed, insert)
|
||||
|
||||
fun ellipse(
|
||||
center: Vector2,
|
||||
xRadius: Double,
|
||||
yRadius: Double,
|
||||
rotationInDegrees: Double,
|
||||
closed: Boolean = true,
|
||||
insert: Boolean = true
|
||||
) = contour(OrientedEllipse(center, xRadius, yRadius, rotationInDegrees).contour.let {
|
||||
if (closed) {
|
||||
it
|
||||
} else {
|
||||
it.open
|
||||
}
|
||||
}, insert)
|
||||
*/
|
||||
|
||||
|
||||
fun lineSegment(
|
||||
startX: Double,
|
||||
startY: Double,
|
||||
endX: Double,
|
||||
endY: Double,
|
||||
insert: Boolean = true
|
||||
) = lineSegment(LineSegment(startX, startY, endX, endY), insert)
|
||||
|
||||
fun lineSegment(
|
||||
start: Vector2,
|
||||
end: Vector2,
|
||||
insert: Boolean = true
|
||||
) = lineSegment(LineSegment(start, end), insert)
|
||||
|
||||
fun lineSegment(
|
||||
lineSegment: LineSegment,
|
||||
insert: Boolean = true
|
||||
) = contour(lineSegment.contour, insert)
|
||||
|
||||
fun lineSegments(
|
||||
lineSegments: List<LineSegment>,
|
||||
insert: Boolean = true
|
||||
) = lineSegments.map {
|
||||
lineSegment(it, insert)
|
||||
}
|
||||
|
||||
fun segment(
|
||||
start: Vector2,
|
||||
c0: Vector2,
|
||||
c1: Vector2,
|
||||
end: Vector2,
|
||||
insert: Boolean = true
|
||||
) = segment(Segment(start, c0, c1, end), insert)
|
||||
|
||||
fun segment(
|
||||
start: Vector2,
|
||||
c0: Vector2,
|
||||
end: Vector2,
|
||||
insert: Boolean = true
|
||||
) = segment(Segment(start, c0, end), insert)
|
||||
|
||||
fun segment(
|
||||
start: Vector2,
|
||||
end: Vector2,
|
||||
insert: Boolean = true
|
||||
) = segment(Segment(start, end), insert)
|
||||
|
||||
fun segment(
|
||||
segment: Segment,
|
||||
insert: Boolean = true
|
||||
) = contour(segment.contour, insert)
|
||||
|
||||
fun segments(
|
||||
segments: List<Segment>,
|
||||
insert: Boolean = true
|
||||
) = segments.map {
|
||||
segment(it, insert)
|
||||
}
|
||||
|
||||
fun lineStrip(
|
||||
points: List<Vector2>,
|
||||
insert: Boolean = true
|
||||
) = contour(ShapeContour.fromPoints(points, false, YPolarity.CW_NEGATIVE_Y), insert)
|
||||
|
||||
fun lineLoop(
|
||||
points: List<Vector2>,
|
||||
insert: Boolean = true
|
||||
) = contour(ShapeContour.fromPoints(points, true, YPolarity.CW_NEGATIVE_Y), insert)
|
||||
|
||||
fun text(
|
||||
text: String,
|
||||
position: Vector2,
|
||||
insert: Boolean = true
|
||||
): TextNode {
|
||||
val g = GroupNode()
|
||||
g.style.transform = Transform.Matrix(transform { translate(position.xy0) })
|
||||
val textNode = TextNode(text, null).apply {
|
||||
this.style.fill = when (val f = this@CompositionDrawer.fill) {
|
||||
is ColorRGBa -> Paint.RGB(f)
|
||||
else -> Paint.None
|
||||
}
|
||||
|
||||
}
|
||||
g.children.add(textNode)
|
||||
if (insert) {
|
||||
cursor.children.add(g)
|
||||
}
|
||||
return textNode
|
||||
}
|
||||
|
||||
fun textOnContour(
|
||||
text: String,
|
||||
contour: ShapeContour,
|
||||
insert: Boolean = true
|
||||
): TextNode {
|
||||
val textNode = TextNode(text, contour)
|
||||
if (insert) {
|
||||
cursor.children.add(textNode)
|
||||
}
|
||||
return textNode
|
||||
}
|
||||
|
||||
fun texts(text: List<String>, positions: List<Vector2>) =
|
||||
(text zip positions).map {
|
||||
text(it.first, it.second)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an image to the composition tree
|
||||
*/
|
||||
fun image(
|
||||
image: ColorBuffer,
|
||||
x: Double = 0.0,
|
||||
y: Double = 0.0,
|
||||
insert: Boolean = true
|
||||
): ImageNode {
|
||||
val node = ImageNode(image, x, y, width = image.width.toDouble(), height = image.height.toDouble())
|
||||
node.style.transform = Transform.Matrix(this.model)
|
||||
if (insert) {
|
||||
cursor.children.add(node)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
fun composition(composition: Composition): CompositionNode {
|
||||
val rootContainer = GroupNode()
|
||||
val newRoot = composition.root.duplicate(insert = false)
|
||||
newRoot.parent = rootContainer
|
||||
rootContainer.children.add(newRoot)
|
||||
rootContainer.transform *= model
|
||||
rootContainer.parent = cursor
|
||||
cursor.children.add(rootContainer)
|
||||
|
||||
return rootContainer
|
||||
}
|
||||
|
||||
fun CompositionNode.translate(x: Double, y: Double, z: Double = 0.0) {
|
||||
transform = transform.transform {
|
||||
translate(x, y, z)
|
||||
}
|
||||
}
|
||||
|
||||
fun CompositionNode.rotate(angleInDegrees: Double, pivot: Vector2 = Vector2.ZERO) {
|
||||
transform = transform.transform {
|
||||
translate(pivot.xy0)
|
||||
rotate(Vector3.UNIT_Z, angleInDegrees)
|
||||
translate(-pivot.xy0)
|
||||
}
|
||||
}
|
||||
|
||||
fun CompositionNode.scale(scale: Double, pivot: Vector2 = Vector2.ZERO) {
|
||||
transform = transform.transform {
|
||||
translate(pivot.xy0)
|
||||
scale(scale, scale, scale)
|
||||
translate(-pivot.xy0)
|
||||
}
|
||||
}
|
||||
|
||||
fun CompositionNode.transform(builder: TransformBuilder.() -> Unit) {
|
||||
return this.transform(builder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a deep copy of a [CompositionNode].
|
||||
* If [insert] is true the copy is inserted at [cursor].
|
||||
* @return a deep copy of the node
|
||||
*/
|
||||
// TODO: Include new features
|
||||
fun CompositionNode.duplicate(insert: Boolean = true): CompositionNode {
|
||||
fun nodeCopy(node: CompositionNode): CompositionNode {
|
||||
val copy = when (node) {
|
||||
is ImageNode -> {
|
||||
ImageNode(node.image, node.x, node.y, node.width, node.height)
|
||||
}
|
||||
is ShapeNode -> {
|
||||
ShapeNode(node.shape)
|
||||
}
|
||||
is TextNode -> {
|
||||
TextNode(node.text, node.contour)
|
||||
}
|
||||
is GroupNode -> {
|
||||
val children = node.children.map { nodeCopy(it) }.toMutableList()
|
||||
val groupNode = GroupNode(children)
|
||||
groupNode.children.forEach {
|
||||
it.parent = groupNode
|
||||
}
|
||||
groupNode
|
||||
}
|
||||
}
|
||||
copy.style = node.style
|
||||
return copy
|
||||
}
|
||||
|
||||
val copy = nodeCopy(this)
|
||||
if (insert) {
|
||||
this@CompositionDrawer.cursor.children.add(copy)
|
||||
copy.parent = cursor
|
||||
}
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [Composition]. The draw operations contained inside
|
||||
* the [drawFunction] do not render graphics to the screen,
|
||||
* but populate the Composition instead.
|
||||
*/
|
||||
fun drawComposition(
|
||||
documentBounds: CompositionDimensions = defaultCompositionDimensions,
|
||||
composition: Composition? = null,
|
||||
cursor: GroupNode? = composition?.root as? GroupNode,
|
||||
drawFunction: CompositionDrawer.() -> Unit
|
||||
): Composition = CompositionDrawer(documentBounds, composition, cursor).apply { drawFunction() }.composition
|
||||
|
||||
/**
|
||||
* Draw into an existing [Composition].
|
||||
*/
|
||||
fun Composition.draw(drawFunction: CompositionDrawer.() -> Unit) {
|
||||
drawComposition(composition = this, drawFunction = drawFunction)
|
||||
}
|
||||
430
orx-composition/src/commonMain/kotlin/CompositionStyleSheet.kt
Normal file
430
orx-composition/src/commonMain/kotlin/CompositionStyleSheet.kt
Normal file
@@ -0,0 +1,430 @@
|
||||
@file:Suppress("RemoveExplicitTypeArguments")
|
||||
|
||||
package org.openrndr.extra.composition
|
||||
import org.openrndr.color.*
|
||||
import org.openrndr.draw.*
|
||||
import org.openrndr.extra.composition.AttributeOrPropertyKey.*
|
||||
import org.openrndr.extra.composition.Inheritance.*
|
||||
import org.openrndr.math.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
import kotlin.reflect.*
|
||||
|
||||
enum class Inheritance {
|
||||
INHERIT,
|
||||
RESET
|
||||
}
|
||||
|
||||
sealed interface AttributeOrPropertyValue {
|
||||
val value: Any?
|
||||
override fun toString(): String
|
||||
}
|
||||
|
||||
sealed interface Paint : AttributeOrPropertyValue {
|
||||
override val value: ColorRGBa?
|
||||
|
||||
class RGB(override val value: ColorRGBa) : Paint {
|
||||
override fun toString(): String {
|
||||
val hexs = listOf(value.r, value.g, value.b).map {
|
||||
(it.coerceIn(0.0, 1.0) * 255.0).toInt().toString(16).padStart(2, '0')
|
||||
}
|
||||
return hexs.joinToString(prefix = "#", separator = "")
|
||||
}
|
||||
}
|
||||
|
||||
// This one is kept just in case, it's not handled in any way yet
|
||||
object CurrentColor : Paint {
|
||||
override val value: ColorRGBa
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun toString(): String = "currentcolor"
|
||||
}
|
||||
|
||||
object None : Paint {
|
||||
override val value: ColorRGBa? = null
|
||||
override fun toString(): String = "none"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Shade : AttributeOrPropertyValue {
|
||||
override val value: ShadeStyle
|
||||
|
||||
class Value(override val value: ShadeStyle) : Shade {
|
||||
override fun toString(): String = ""
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Length : AttributeOrPropertyValue {
|
||||
override val value: Double
|
||||
|
||||
class Pixels(override val value: Double) : Length {
|
||||
companion object {
|
||||
fun fromInches(value: Double) = Pixels(value * 96.0)
|
||||
fun fromPicas(value: Double) = Pixels(value * 16.0)
|
||||
fun fromPoints(value: Double) = Pixels(value * (4.0 / 3.0))
|
||||
fun fromCentimeters(value: Double) = Pixels(value * (96.0 / 2.54))
|
||||
fun fromMillimeters(value: Double) = Pixels(value * (96.0 / 25.4))
|
||||
fun fromQuarterMillimeters(value: Double) = Pixels(value * (96.0 / 101.6))
|
||||
}
|
||||
|
||||
override fun toString(): String = "$value"
|
||||
}
|
||||
|
||||
class Percent(override val value: Double) : Length {
|
||||
override fun toString(): String {
|
||||
return "${value}%"
|
||||
}
|
||||
}
|
||||
|
||||
enum class UnitIdentifier {
|
||||
IN,
|
||||
PC,
|
||||
PT,
|
||||
PX,
|
||||
CM,
|
||||
MM,
|
||||
Q
|
||||
}
|
||||
}
|
||||
|
||||
inline val Double.pixels: Length.Pixels
|
||||
get() = Length.Pixels(this)
|
||||
inline val Double.percent: Length.Percent
|
||||
get() = Length.Percent(this)
|
||||
|
||||
sealed interface Numeric : AttributeOrPropertyValue {
|
||||
override val value: Double
|
||||
|
||||
class Rational(override val value: Double) : Numeric {
|
||||
override fun toString(): String = "$value"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Transform : AttributeOrPropertyValue {
|
||||
override val value: Matrix44
|
||||
|
||||
class Matrix(override val value: Matrix44) : Transform {
|
||||
override fun toString(): String {
|
||||
return if (value == Matrix44.IDENTITY) {
|
||||
""
|
||||
} else {
|
||||
"matrix(${value.c0r0} ${value.c0r1} " +
|
||||
"${value.c1r0} ${value.c1r1} " +
|
||||
"${value.c3r0} ${value.c3r1})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object None : Transform {
|
||||
override val value = Matrix44.IDENTITY
|
||||
override fun toString(): String = ""
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Visibility : AttributeOrPropertyValue {
|
||||
override val value: Boolean
|
||||
|
||||
object Visible : Visibility {
|
||||
override val value = true
|
||||
override fun toString() = "visible"
|
||||
}
|
||||
|
||||
object Hidden : Visibility {
|
||||
override val value = false
|
||||
override fun toString() = "hidden"
|
||||
}
|
||||
|
||||
// This exists because the spec specifies so,
|
||||
// it is effectively Hidden.
|
||||
object Collapse : Visibility {
|
||||
override val value = false
|
||||
override fun toString() = "collapse"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Display : AttributeOrPropertyValue {
|
||||
override val value: Boolean
|
||||
|
||||
object Inline : Display {
|
||||
override val value = true
|
||||
override fun toString() = "inline"
|
||||
}
|
||||
|
||||
object Block : Display {
|
||||
override val value = true
|
||||
override fun toString() = "block"
|
||||
}
|
||||
|
||||
object None : Display {
|
||||
override val value = false
|
||||
override fun toString() = "none"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface LineCap : AttributeOrPropertyValue {
|
||||
override val value: org.openrndr.draw.LineCap
|
||||
|
||||
object Round : LineCap {
|
||||
override val value = org.openrndr.draw.LineCap.ROUND
|
||||
override fun toString() = "round"
|
||||
}
|
||||
|
||||
object Butt : LineCap {
|
||||
override val value = org.openrndr.draw.LineCap.BUTT
|
||||
override fun toString() = "butt"
|
||||
}
|
||||
|
||||
object Square : LineCap {
|
||||
override val value = org.openrndr.draw.LineCap.SQUARE
|
||||
override fun toString() = "square"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface LineJoin : AttributeOrPropertyValue {
|
||||
override val value: org.openrndr.draw.LineJoin
|
||||
|
||||
object Miter : LineJoin {
|
||||
override val value = org.openrndr.draw.LineJoin.MITER
|
||||
override fun toString() = "miter"
|
||||
}
|
||||
|
||||
object Bevel : LineJoin {
|
||||
override val value = org.openrndr.draw.LineJoin.BEVEL
|
||||
override fun toString() = "bevel"
|
||||
}
|
||||
|
||||
object Round : LineJoin {
|
||||
override val value = org.openrndr.draw.LineJoin.ROUND
|
||||
override fun toString() = "round"
|
||||
}
|
||||
}
|
||||
|
||||
enum class Align {
|
||||
NONE,
|
||||
X_MIN_Y_MIN,
|
||||
X_MID_Y_MIN,
|
||||
X_MAX_Y_MIN,
|
||||
X_MIN_Y_MID,
|
||||
X_MID_Y_MID,
|
||||
X_MAX_Y_MID,
|
||||
X_MIN_Y_MAX,
|
||||
X_MID_Y_MAX,
|
||||
X_MAX_Y_MAX
|
||||
}
|
||||
|
||||
enum class MeetOrSlice {
|
||||
MEET,
|
||||
SLICE
|
||||
}
|
||||
|
||||
data class AspectRatio(val align: Align, val meetOrSlice: MeetOrSlice) : AttributeOrPropertyValue {
|
||||
override val value = this
|
||||
|
||||
companion object {
|
||||
val DEFAULT = AspectRatio(Align.X_MID_Y_MID, MeetOrSlice.MEET)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
if (this == DEFAULT) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val a = when (align) {
|
||||
Align.NONE -> "none"
|
||||
Align.X_MIN_Y_MIN -> "xMinYMin"
|
||||
Align.X_MID_Y_MIN -> "xMidYMin"
|
||||
Align.X_MAX_Y_MIN -> "xMaxYMin"
|
||||
Align.X_MIN_Y_MID -> "xMinYMid"
|
||||
Align.X_MID_Y_MID -> "xMidYMid"
|
||||
Align.X_MAX_Y_MID -> "xMaxYMid"
|
||||
Align.X_MIN_Y_MAX -> "xMinYMax"
|
||||
Align.X_MID_Y_MAX -> "xMidYMax"
|
||||
Align.X_MAX_Y_MAX -> "xMaxYMax"
|
||||
}
|
||||
val m = when (meetOrSlice) {
|
||||
MeetOrSlice.MEET -> "meet"
|
||||
MeetOrSlice.SLICE -> "slice"
|
||||
}
|
||||
|
||||
return "$a $m"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ViewBox : AttributeOrPropertyValue {
|
||||
override val value: Rectangle?
|
||||
|
||||
class Value(override val value: Rectangle) : ViewBox {
|
||||
override fun toString(): String =
|
||||
"${value.x.toInt()} ${value.y.toInt()} ${value.width.toInt()} ${value.height.toInt()}"
|
||||
}
|
||||
|
||||
/**
|
||||
* The viewBox has not been defined,
|
||||
* **not** that it doesn't exist.
|
||||
*/
|
||||
object None : ViewBox {
|
||||
override val value: Rectangle? = null
|
||||
override fun toString(): String = ""
|
||||
}
|
||||
}
|
||||
|
||||
private data class PropertyBehavior(val inherit: Inheritance, val initial: AttributeOrPropertyValue)
|
||||
|
||||
private object PropertyBehaviors {
|
||||
val behaviors = HashMap<AttributeOrPropertyKey, PropertyBehavior>()
|
||||
}
|
||||
|
||||
private class PropertyDelegate<T : AttributeOrPropertyValue>(
|
||||
val name: AttributeOrPropertyKey,
|
||||
inheritance: Inheritance,
|
||||
val initial: T
|
||||
) {
|
||||
init {
|
||||
PropertyBehaviors.behaviors[name] = PropertyBehavior(inheritance, initial)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
operator fun getValue(style: Styleable, property: KProperty<*>): T {
|
||||
return (style[name] ?: PropertyBehaviors.behaviors[name]!!.initial) as T
|
||||
}
|
||||
|
||||
operator fun setValue(style: Styleable, property: KProperty<*>, value: T?) {
|
||||
style[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Styleable {
|
||||
val properties = HashMap<AttributeOrPropertyKey, AttributeOrPropertyValue?>()
|
||||
|
||||
operator fun get(name: AttributeOrPropertyKey) = properties[name]
|
||||
|
||||
operator fun set(name: AttributeOrPropertyKey, value: AttributeOrPropertyValue?) {
|
||||
properties[name] = value
|
||||
}
|
||||
|
||||
infix fun inherit(from: Style): Style {
|
||||
return Style().also {
|
||||
from.properties.forEach { (name, value) ->
|
||||
if (PropertyBehaviors.behaviors[name]?.inherit == INHERIT) {
|
||||
it.properties[name] = value
|
||||
}
|
||||
}
|
||||
it.properties.putAll(properties)
|
||||
}
|
||||
}
|
||||
|
||||
// Because AttributeOrPropertyValue has a toString override,
|
||||
// we can abuse it for equality checks.
|
||||
fun isInherited(from: Styleable, attributeKey: AttributeOrPropertyKey): Boolean =
|
||||
when (this.properties[attributeKey].toString()) {
|
||||
from.properties[attributeKey].toString() -> true
|
||||
PropertyBehaviors.behaviors[attributeKey]?.initial.toString() -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentStyle : Styleable()
|
||||
class Style : Styleable()
|
||||
|
||||
var DocumentStyle.viewBox by PropertyDelegate<ViewBox>(VIEW_BOX, RESET, ViewBox.None)
|
||||
var DocumentStyle.preserveAspectRatio by PropertyDelegate<AspectRatio>(
|
||||
PRESERVE_ASPECT_RATIO,
|
||||
RESET, AspectRatio.DEFAULT
|
||||
)
|
||||
|
||||
var Style.stroke by PropertyDelegate<Paint>(STROKE, INHERIT, Paint.None)
|
||||
var Style.strokeOpacity by PropertyDelegate<Numeric>(STROKE_OPACITY, INHERIT, Numeric.Rational(1.0))
|
||||
var Style.strokeWeight by PropertyDelegate<Length>(STROKE_WIDTH, INHERIT, 1.0.pixels)
|
||||
var Style.miterLimit by PropertyDelegate<Numeric>(STROKE_MITERLIMIT, INHERIT, Numeric.Rational(4.0))
|
||||
var Style.lineCap by PropertyDelegate<LineCap>(STROKE_LINECAP, INHERIT, LineCap.Butt)
|
||||
var Style.lineJoin by PropertyDelegate<LineJoin>(STROKE_LINEJOIN, INHERIT, LineJoin.Miter)
|
||||
|
||||
var Style.fill by PropertyDelegate<Paint>(FILL, INHERIT, Paint.RGB(ColorRGBa.BLACK))
|
||||
var Style.fillOpacity by PropertyDelegate<Numeric>(FILL_OPACITY, INHERIT, Numeric.Rational(1.0))
|
||||
|
||||
var Style.transform by PropertyDelegate<Transform>(TRANSFORM, RESET, Transform.None)
|
||||
|
||||
// Okay so the spec says `display` isn't inheritable, but effectively acts so
|
||||
// when the element and its children are excluded from the rendering tree.
|
||||
var Style.display by PropertyDelegate<Display>(DISPLAY, RESET, Display.Inline)
|
||||
var Style.opacity by PropertyDelegate<Numeric>(OPACITY, RESET, Numeric.Rational(1.0))
|
||||
var Style.visibility by PropertyDelegate<Visibility>(VISIBILITY, INHERIT, Visibility.Visible)
|
||||
|
||||
var Style.x by PropertyDelegate<Length>(X, RESET, 0.0.pixels)
|
||||
var Style.y by PropertyDelegate<Length>(Y, RESET, 0.0.pixels)
|
||||
var Style.width by PropertyDelegate<Length>(WIDTH, RESET, 768.0.pixels)
|
||||
var Style.height by PropertyDelegate<Length>(HEIGHT, RESET, 576.0.pixels)
|
||||
|
||||
var Style.shadeStyle by PropertyDelegate<Shade>(SHADESTYLE, INHERIT, Shade.Value(ShadeStyle()))
|
||||
|
||||
enum class AttributeOrPropertyKey {
|
||||
// @formatter:off
|
||||
// Attributes
|
||||
BASE_PROFILE { override fun toString() = "baseProfile" },
|
||||
CLASS { override fun toString() = "class" },
|
||||
CX { override fun toString() = "cx" },
|
||||
CY { override fun toString() = "cy" },
|
||||
D { override fun toString() = "d" },
|
||||
DX { override fun toString() = "dx" },
|
||||
DY { override fun toString() = "dy" },
|
||||
GRADIENT_UNITS { override fun toString() = "gradientUnits" },
|
||||
HEIGHT { override fun toString() = "height" },
|
||||
ID { override fun toString() = "id" },
|
||||
OFFSET { override fun toString() = "offset" },
|
||||
PATH_LENGTH { override fun toString() = "pathLength" },
|
||||
POINTS { override fun toString() = "points" },
|
||||
PRESERVE_ASPECT_RATIO { override fun toString() = "preserveAspectRatio" },
|
||||
R { override fun toString() = "r" },
|
||||
ROTATE { override fun toString() = "rotate" },
|
||||
RX { override fun toString() = "rx" },
|
||||
RY { override fun toString() = "ry" },
|
||||
SPACE { override fun toString() = "xml:space" },
|
||||
STYLE { override fun toString() = "style" },
|
||||
TRANSFORM { override fun toString() = "transform" },
|
||||
VERSION { override fun toString() = "version" },
|
||||
VIEW_BOX { override fun toString() = "viewBox" },
|
||||
WIDTH { override fun toString() = "width" },
|
||||
X { override fun toString() = "x" },
|
||||
X1 { override fun toString() = "x1" },
|
||||
X2 { override fun toString() = "x2" },
|
||||
Y { override fun toString() = "y" },
|
||||
Y1 { override fun toString() = "y1" },
|
||||
Y2 { override fun toString() = "y2" },
|
||||
|
||||
// Properties
|
||||
COLOR { override fun toString() = "color" },
|
||||
DIRECTION { override fun toString() = "direction" },
|
||||
DISPLAY { override fun toString() = "display" },
|
||||
DISPLAY_ALIGN { override fun toString() = "display-align" },
|
||||
FILL { override fun toString() = "fill" },
|
||||
FILL_OPACITY { override fun toString() = "fill-opacity" },
|
||||
FILL_RULE { override fun toString() = "fill-rule" },
|
||||
FONT_FAMILY { override fun toString() = "font-family" },
|
||||
FONT_SIZE { override fun toString() = "font-size" },
|
||||
FONT_STYLE { override fun toString() = "font-style" },
|
||||
FONT_VARIANT { override fun toString() = "font-variant" },
|
||||
FONT_WEIGHT { override fun toString() = "font-weight" },
|
||||
OPACITY { override fun toString() = "opacity" },
|
||||
STOP_COLOR { override fun toString() = "stop-color" },
|
||||
STOP_OPACITY { override fun toString() = "stop-opacity" },
|
||||
STROKE { override fun toString() = "stroke" },
|
||||
STROKE_DASHARRAY { override fun toString() = "stroke-dasharray" },
|
||||
STROKE_DASHOFFSET { override fun toString() = "stroke-dashoffset" },
|
||||
STROKE_LINECAP { override fun toString() = "stroke-linecap" },
|
||||
STROKE_LINEJOIN { override fun toString() = "stroke-linejoin" },
|
||||
STROKE_MITERLIMIT { override fun toString() = "stroke-miterlimit" },
|
||||
STROKE_OPACITY { override fun toString() = "stroke-opacity" },
|
||||
STROKE_WIDTH { override fun toString() = "stroke-width" },
|
||||
TEXT_ALIGN { override fun toString() = "text-align" },
|
||||
TEXT_ANCHOR { override fun toString() = "text-anchor" },
|
||||
UNICODE_BIDI { override fun toString() = "unicode-bidi" },
|
||||
VECTOR_EFFECT { override fun toString() = "vector-effect" },
|
||||
VISIBILITY { override fun toString() = "visibility" },
|
||||
|
||||
// Made-up properties
|
||||
// because "Compositions aren't SVGs and yadda yadda"
|
||||
// this one's for you, edwin
|
||||
SHADESTYLE { override fun toString() = "" };
|
||||
|
||||
abstract override fun toString(): String
|
||||
// @formatter:on
|
||||
}
|
||||
94
orx-composition/src/commonMain/kotlin/DrawerExtensions.kt
Normal file
94
orx-composition/src/commonMain/kotlin/DrawerExtensions.kt
Normal file
@@ -0,0 +1,94 @@
|
||||
package org.openrndr.extra.composition
|
||||
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.shape.*
|
||||
|
||||
/**
|
||||
* Draws a [Composition]
|
||||
* @param composition The composition to draw
|
||||
* @see contour
|
||||
* @see contours
|
||||
* @see shape
|
||||
* @see shapes
|
||||
*/
|
||||
fun Drawer.composition(composition: Composition) {
|
||||
pushModel()
|
||||
pushStyle()
|
||||
|
||||
// viewBox transformation
|
||||
model *= composition.calculateViewportTransform()
|
||||
|
||||
fun node(compositionNode: CompositionNode) {
|
||||
pushModel()
|
||||
pushStyle()
|
||||
model *= compositionNode.style.transform.value
|
||||
|
||||
shadeStyle = (compositionNode.style.shadeStyle as Shade.Value).value
|
||||
|
||||
when (compositionNode) {
|
||||
is ShapeNode -> {
|
||||
|
||||
compositionNode.style.stroke.let {
|
||||
stroke = when (it) {
|
||||
is Paint.RGB -> it.value.copy(alpha = 1.0)
|
||||
Paint.None -> null
|
||||
Paint.CurrentColor -> null
|
||||
}
|
||||
}
|
||||
compositionNode.style.strokeOpacity.let {
|
||||
stroke = when (it) {
|
||||
is Numeric.Rational -> stroke?.opacify(it.value)
|
||||
}
|
||||
}
|
||||
compositionNode.style.strokeWeight.let {
|
||||
strokeWeight = when (it) {
|
||||
is Length.Pixels -> it.value
|
||||
is Length.Percent -> composition.normalizedDiagonalLength() * it.value / 100.0
|
||||
}
|
||||
}
|
||||
compositionNode.style.miterLimit.let {
|
||||
miterLimit = when (it) {
|
||||
is Numeric.Rational -> it.value
|
||||
}
|
||||
}
|
||||
compositionNode.style.lineCap.let {
|
||||
lineCap = it.value
|
||||
}
|
||||
compositionNode.style.lineJoin.let {
|
||||
lineJoin = it.value
|
||||
}
|
||||
compositionNode.style.fill.let {
|
||||
fill = when (it) {
|
||||
is Paint.RGB -> it.value.copy(alpha = 1.0)
|
||||
is Paint.None -> null
|
||||
is Paint.CurrentColor -> null
|
||||
}
|
||||
}
|
||||
compositionNode.style.fillOpacity.let {
|
||||
fill = when (it) {
|
||||
is Numeric.Rational -> fill?.opacify(it.value)
|
||||
}
|
||||
}
|
||||
compositionNode.style.opacity.let {
|
||||
when (it) {
|
||||
is Numeric.Rational -> {
|
||||
stroke = stroke?.opacify(it.value)
|
||||
fill = fill?.opacify(it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
shape(compositionNode.shape)
|
||||
}
|
||||
is ImageNode -> {
|
||||
image(compositionNode.image)
|
||||
}
|
||||
is TextNode -> TODO()
|
||||
is GroupNode -> compositionNode.children.forEach { node(it) }
|
||||
}
|
||||
popModel()
|
||||
popStyle()
|
||||
}
|
||||
node(composition.root)
|
||||
popModel()
|
||||
popStyle()
|
||||
}
|
||||
36
orx-composition/src/commonMain/kotlin/ProgramExtensions.kt
Normal file
36
orx-composition/src/commonMain/kotlin/ProgramExtensions.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package org.openrndr.extra.composition
|
||||
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.shape.Rectangle
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
// Derives Composition dimensions from current Drawer
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun Program.drawComposition(
|
||||
documentBounds: CompositionDimensions = CompositionDimensions(0.0.pixels, 0.0.pixels, this.drawer.width.toDouble().pixels, this.drawer.height.toDouble().pixels),
|
||||
composition: Composition? = null,
|
||||
cursor: GroupNode? = composition?.root as? GroupNode,
|
||||
drawFunction: CompositionDrawer.() -> Unit
|
||||
): Composition {
|
||||
contract {
|
||||
callsInPlace(drawFunction, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return CompositionDrawer(documentBounds, composition, cursor).apply { drawFunction() }.composition
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun Program.drawComposition(
|
||||
documentBounds: Rectangle,
|
||||
composition: Composition? = null,
|
||||
cursor: GroupNode? = composition?.root as? GroupNode,
|
||||
drawFunction: CompositionDrawer.() -> Unit
|
||||
): Composition {
|
||||
contract {
|
||||
callsInPlace(drawFunction, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return CompositionDrawer(CompositionDimensions(documentBounds), composition, cursor).apply { drawFunction() }.composition
|
||||
}
|
||||
|
||||
|
||||
38
orx-composition/src/commonTest/kotlin/TestComposition.kt
Normal file
38
orx-composition/src/commonTest/kotlin/TestComposition.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package org.openrndr.extra.composition
|
||||
|
||||
import org.openrndr.shape.Shape
|
||||
import kotlin.test.*
|
||||
|
||||
class TestComposition {
|
||||
val composition = let { _ ->
|
||||
val root = GroupNode().also { it.id = "outer" }
|
||||
root.children += GroupNode().also {
|
||||
it.id = "inner"
|
||||
}
|
||||
root.children += ShapeNode(Shape.EMPTY).also {
|
||||
it.id = "shape"
|
||||
}
|
||||
Composition(root)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun findGroup() {
|
||||
assertEquals("outer", composition.findGroup("outer")?.id)
|
||||
assertEquals("inner", composition.findGroup("inner")?.id)
|
||||
assertNull(composition.findGroup("shape"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun findShape() {
|
||||
assertEquals("shape", composition.findShape("shape")?.id)
|
||||
assertNull(composition.findShape("inner"))
|
||||
assertNull(composition.findShape("outer"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun findImage() {
|
||||
assertNull(composition.findImage("inner"))
|
||||
assertNull(composition.findImage("outer"))
|
||||
assertNull(composition.findImage("shape"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user