[orx-shade-styles] Add advanced clip-based shade styles and demos

This commit is contained in:
Edwin Jakobs
2025-03-04 08:38:10 +01:00
parent e7f11d90b2
commit dc09a849fd
5 changed files with 591 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
package org.openrndr.extra.shadestyles.fills.clip
import org.openrndr.draw.ShadeStyle
class ClipbaseStructure(
val clipFunction: String,
val domainWarpFunction: String
)
class ClipBase(structure: ClipbaseStructure) : ShadeStyle() {
var clipUnits: Int by Parameter()
var clipFit: Int by Parameter()
init {
fragmentPreamble = """
${structure.domainWarpFunction}
${structure.clipFunction}
""".trimIndent()
fragmentTransform = """vec2 coord = vec2(0.0);
if (p_clipUnits == 0) { // BOUNDS
coord = c_boundsPosition.xy;
if (p_clipFit == 1) { // COVER
float mx = max(c_boundsSize.x, c_boundsSize.y);
float ar = min(c_boundsSize.x, c_boundsSize.y) / mx;
if (c_boundsSize.x == mx) {
coord.y = (coord.y - 0.5) * ar + 0.5;
} else {
coord.x = (coord.x - 0.5) * ar + 0.5;
}
} else if (p_clipFit == 2) { // CONTAIN
float mx = max(c_boundsSize.x, c_boundsSize.y);
float ar = mx / min(c_boundsSize.x, c_boundsSize.y);
if (c_boundsSize.y == mx) {
coord.y = (coord.y - 0.5) * ar + 0.5;
} else {
coord.x = (coord.x - 0.5) * ar + 0.5;
}
}
} else if (p_clipUnits == 1) { // WORLD
coord = v_worldPosition.xy;
} else if (p_clipUnits == 2) { // VIEW
coord = v_viewPosition.xy;
} else if (p_clipUnits == 3) { // SCREEN
coord = c_screenPosition.xy;
coord.y = u_viewDimensions.y - coord.y;
}
coord = clipDomainWarp(coord);
coord = (p_clipTransform * vec4(coord, 0.0, 1.0)).xy;
float mask = clipMask(coord);
if (p_clipInvert) {
mask = -mask;
}
float maskWidth = abs(fwidth(mask));
float maskFiltered = clamp(p_clipFloor +
p_clipBlend *
smoothstep(maskWidth * 0.5, -maskWidth * 0.5 - p_clipFeather, mask - p_clipOuter) *
smoothstep(-maskWidth * 0.5, maskWidth * 0.5 + p_clipFeather, mask - p_clipInner),
0.0, 1.0);
x_fill.a *= maskFiltered;
x_stroke.a *= maskFiltered;
""".trimIndent()
}
}

View File

@@ -0,0 +1,350 @@
package org.openrndr.extra.shadestyles.fills.clip
import org.openrndr.draw.ObservableHashmap
import org.openrndr.draw.StyleParameters
import org.openrndr.extra.shaderphrases.sdf.sdEllipsePhrase
import org.openrndr.extra.shaderphrases.sdf.sdStarPhrase
import org.openrndr.extra.shadestyles.fills.FillFit
import org.openrndr.extra.shadestyles.fills.FillUnits
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector4
import org.openrndr.shape.Rectangle
class ClipBuilder : StyleParameters {
override var parameterTypes: ObservableHashmap<String, String> = ObservableHashmap(mutableMapOf()) {}
override var parameterValues: MutableMap<String, Any> = mutableMapOf()
override var textureBaseIndex: Int = 2
/**
* Specifies the outer threshold for the clipping effect in rendering.
*
* Default value is `0.0`.
*/
var clipOuter: Double by Parameter("clipOuter", initialValue = 0.0)
/**
* Defines the inner boundary threshold for applying a clipping mask.
*
* The initial value is set to a very large negative number
* (-1E12) to enable a wide default range.
*/
var clipInner: Double by Parameter("clipInner", initialValue = -1E12)
/**
* Specifies the coordinate space used for defining clip shapes.
* The value can be one of the `FillUnits` enum constants, such as:
* - `BOUNDS`: The coordinate space is based on the local bounds of the object.
* - `WORLD`: The coordinate space is based on the world position.
* - `VIEW`: The coordinate space is based on the view or camera position.
* - `SCREEN`: The coordinate space is based on the screen space.
*
* Determines how the clip shapes are positioned and sized relative to the object being clipped.
* The default value is `BOUNDS`.
*/
var clipUnits: FillUnits = FillUnits.BOUNDS
/**
* Defines the strategy for fitting a clip shape to its bounds.
*
* This property determines how the clip shape will be scaled or resized to fit
* within a bounding area. The possible values are specified by the [FillFit]
* enumeration:
*
* - [FillFit.STRETCH]: Stretches the clip shape to fully cover the bounds, ignoring original aspect ratio.
* - [FillFit.COVER]: Scales the clip shape to completely cover the bounds while maintaining its aspect ratio.
* - [FillFit.CONTAIN]: Scales the clip shape to fully fit within the bounds while preserving its aspect ratio.
*
* The default fit mode is [FillFit.STRETCH].
*/
var clipFit: FillFit = FillFit.STRETCH
/**
* Represents the GLSL function used to define the clipping mask for a shader.
*
* The `clipMaskFunction` defines a GLSL function `clipMask` that takes a `vec2` coordinate as input
* and outputs a `float` value. The function is intended to determine how the clipping is applied
* based on the input coordinate. The return value typically indicates the mask's boundaries, such
* as a signed distance to a shape.
*
* This property can be customized to define various shapes for clipping masks by overwriting it
* with specific GLSL code.
*
* The default implementation of the `clipMaskFunction` always returns `-1.0`, meaning no clipping is
* applied unless overridden.
*/
var clipMaskFunction = """float clipMask(vec2 coord) { return -1.0; }"""
/**
* A variable containing a GLSL function as a string that defines a domain warping operation
* applied to coordinates. Domain warping is typically used to introduce variations or distortions
* in patterns by altering the input space over which a procedure operates.
*
* This variable is used in the shader preamble and the transformation process to
* modify the coordinate system. By default, the `clipDomainWarp` function returns the input
* coordinates unchanged, maintaining an identity operation.
*
* Developers can override this string with a custom GLSL function to create
* specific domain warping effects suited for their application.
*/
var domainWarpFunction = """vec2 clipDomainWarp(vec2 coord) { return coord; }"""
/**
* Specifies the feathering amount applied to the edges of the clipping mask.
* Feathering softens the transition between clipped and unclipped regions, creating a smoother boundary.
* The value represents the width of the feathering effect, with `0.0` indicating no feathering.
*
* A larger value will result in a broader and more gradual transition.
*/
var feather: Double by Parameter("clipFeather", 0.0)
/**
* Determines whether the current clipping mask should be inverted.
*
* When set to `true`, the areas outside of the defined clip mask are rendered instead of the areas inside.
* This parameter is useful for reversing the functionality of a clipping mask,
* such as highlighting areas outside a specified region or shape.
*/
var invert: Boolean by Parameter("clipInvert", false)
var floor: Double by Parameter("clipFloor", 0.0)
var blend: Double by Parameter("clipBlend", 1.0)
var clipTransform: Matrix44 by Parameter("clipTransform", Matrix44.IDENTITY)
private fun structure(): ClipbaseStructure {
return ClipbaseStructure(
clipMaskFunction,
domainWarpFunction
)
}
fun build(): ClipBase {
val clipBase = ClipBase(structure())
clipBase.parameterTypes.putAll(parameterTypes)
clipBase.parameterValues.putAll(parameterValues)
clipBase.clipUnits = clipUnits.ordinal
clipBase.clipFit = clipFit.ordinal
return clipBase
}
/**
* Adds a circular clipping mask to the current clip context.
*
* This method configures a circular clipping region with customizable properties
* defined within the provided builder. The circle is defined by its radius and center.
*
* @param builder a lambda with receiver scope of [CircleClipBuilder] allowing configuration
* of the circle's attributes such as radius and center position.
*/
fun circle(builder: CircleClipBuilder.() -> Unit) {
CircleClipBuilder(this).apply(builder)
}
/**
* Configures a rectangular clipping mask within the current clip context.
*
* This method allows setting up a rectangular clipping region with customizable
* properties defined inside the provided builder.
*
* @param builder a lambda with receiver scope of [RectangleClipBuilder], allowing
* configuration of the rectangle's attributes such as position
* and dimensions.
*/
fun rectangle(builder: RectangleClipBuilder.() -> Unit) {
val b = RectangleClipBuilder(this)
b.apply(builder)
b.clipRectangle = Vector4(b.rectangle.x, b.rectangle.y, b.rectangle.width, b.rectangle.height)
}
/**
* Adds a star-shaped clipping mask to the current clip context.
*
* This method configures a clipping region in the shape of a star with customizable
* properties defined within the provided builder. The star is defined by its radius,
* center, number of sides, and sharpness.
*
* @param builder a lambda with receiver scope of [StarClipBuilder], allowing configuration
* of the star's attributes such as radius, center, sides, and sharpness.
*/
fun star(builder: StarClipBuilder.() -> Unit) {
val b = StarClipBuilder(this)
b.builder()
}
/**
* Adds a line-shaped clipping mask to the current clip context.
*
* This method configures a linear clipping region with customizable properties
* defined within the provided builder. The line is determined by its direction
* and center point.
*
* @param builder a lambda with receiver scope of [LineClipBuilder] allowing
* configuration of the line's attributes such as direction and center position.
*/
fun line(builder: LineClipBuilder.() -> Unit) {
val b = LineClipBuilder(this)
b.builder()
}
/**
* Adds an elliptical clipping mask to the current clip context.
*
* This method configures an elliptical clipping region with customizable properties
* defined within the provided builder. The ellipse is characterized by its horizontal
* and vertical radii, as well as its center position.
*
* @param builder a lambda with receiver scope of [EllipseClipBuilder], allowing
* configuration of the ellipse's attributes such as radiusX, radiusY,
* and center position.
*/
fun ellipse(builder: EllipseClipBuilder.() -> Unit) {
val b = EllipseClipBuilder(this)
b.builder()
}
}
class CircleClipBuilder(builder: ClipBuilder) {
/**
* Defines the radius of the circular clipping mask.
*
* The default value is 0.5.
*/
var radius: Double by builder.Parameter("clipRadius", 0.5)
/**
* Specifies the center of the circular clipping mask in normalized coordinates.
*
* The center is represented as a [Vector2], where the default value is (0.5, 0.5),
* positioning the clipping mask at the center of the target area.
*/
var center: Vector2 by builder.Parameter("clipCenter", Vector2(0.5, 0.5))
init {
builder.clipMaskFunction = """
float clipMask(vec2 coord) {
float d = distance(coord, p_clipCenter);
return d - p_clipRadius;
}
""".trimIndent()
}
}
class RectangleClipBuilder(builder: ClipBuilder) {
/**
* Represents a rectangular clipping region with configurable properties such as
* position and dimensions. The rectangle is defined by its coordinates (x, y),
* width, and height.
*/
var rectangle = Rectangle(0.0, 0.0, 1.0, 1.0)
internal var clipRectangle: Vector4 by builder.Parameter("clipRectangle")
init {
builder.clipMaskFunction = """
float clipMask(vec2 coord) {
vec2 center = p_clipRectangle.xy + p_clipRectangle.zw * 0.5;
vec2 d2 = abs(coord - center) - p_clipRectangle.zw * 0.5;
return max(d2.x, d2.y);
}
""".trimIndent()
}
}
class StarClipBuilder(builder: ClipBuilder) {
/**
* Defines the radius of the star-shaped clip mask.
*/
var radius: Double by builder.Parameter("clipRadius", 0.5)
/**
* Defines the center point of the star-shaped clip mask as a 2D vector.
*/
var center: Vector2 by builder.Parameter("clipCenter", Vector2(0.5, 0.5))
var sides: Int by builder.Parameter("clipSides", 5)
var sharpness: Double by builder.Parameter("clipSharpness", 0.0)
init {
builder.clipMaskFunction = """$sdStarPhrase
float clipMask(vec2 coord) {
float d = sdStar(coord - p_clipCenter, p_clipRadius, p_clipSides, p_clipSharpness);
return d;
}""".trimIndent()
}
}
class EllipseClipBuilder(builder: ClipBuilder) {
/**
* Defines the horizontal radius of the elliptical clipping region.
*
* Defaults to `0.5`.
*/
var radiusX: Double by builder.Parameter("clipRadiusX", 0.5)
/**
* Defines the vertical radius of the elliptical clipping region.
*
* Defaults to `0.5`.
*/
var radiusY: Double by builder.Parameter("clipRadiusY", 0.5)
/**
* The center of the clipping ellipse.
*
* Defaults to (0.5, 0.5)
*/
var center: Vector2 by builder.Parameter("clipCenter", Vector2(0.5, 0.5))
init {
builder.clipMaskFunction = """$sdEllipsePhrase
float clipMask(vec2 coord) {
vec2 d0 = (coord - p_clipCenter);
float d = sdEllipse(d0, vec2(p_clipRadiusX, p_clipRadiusY));
return d;
}
""".trimIndent()
}
}
class LineClipBuilder(builder: ClipBuilder) {
/**
* Represents the direction vector used for clipping operations in the `LineClipBuilder`.
* This value determines the directional component for the clipping mask calculation,
* normalized within the shader implementation.
*
* The default value is `Vector2.UNIT_Y`.
*/
var direction: Vector2 by builder.Parameter("clipDirection", Vector2.UNIT_Y)
/**
* Represents the center point used as a reference for clipping operations in the `LineClipBuilder`.
* This variable defines the central coordinate for the clipping mask relative to the drawable area.
* It is typically normalized within the range [0.0, 1.0], where `Vector2(0.5, 0.5)` corresponds to
* the center of the drawable area.
*
* The default value is `Vector2(0.5, 0.5)`.
*/
var center: Vector2 by builder.Parameter("clipCenter", Vector2(0.5, 0.5))
init {
builder.clipMaskFunction = """
float clipMask(vec2 coord) {
vec2 dir = normalize(p_clipDirection);
float distance = dot(dir, coord - p_clipCenter);
return distance;
}
""".trimIndent()
}
}
/**
* Creates and configures a `ClipBase` object using the specified builder.
*
* @param builder A lambda function used to define the properties of the `ClipBuilder`.
* @return A `ClipBase` instance configured with the specified properties.
*/
fun clip(builder: ClipBuilder.() -> Unit): ClipBase {
return ClipBuilder().apply(builder).build()
}

View File

@@ -0,0 +1,53 @@
package clip
import org.openrndr.application
import org.openrndr.draw.loadImage
import org.openrndr.extra.imageFit.imageFit
import org.openrndr.extra.shadestyles.fills.FillFit
import org.openrndr.extra.shadestyles.fills.clip.clip
import org.openrndr.extra.shapes.primitives.grid
import org.openrndr.math.Vector2
import org.openrndr.math.transforms.transform
import kotlin.math.PI
import kotlin.math.cos
fun main() = application {
configure {
width = 720
height = 720
}
program {
var gf = 0.0
mouse.buttonDown.listen {
gf = 0.5 - gf
}
val image = loadImage("demo-data/images/image-001.png")
extend {
val grid = drawer.bounds.grid(3, 3)
for ((index, cell) in grid.flatten().withIndex()) {
drawer.shadeStyle = clip {
clipFit = FillFit.CONTAIN
feather = gf
clipTransform = transform {
translate(Vector2(0.5, 0.5))
rotate(36.0 * seconds)
translate(Vector2(-0.5, -0.5))
}
star {
radius = 0.5
center = Vector2(0.5, 0.5)
sharpness = cos( 2 * PI * index / 9.0 + seconds) * 0.25 + 0.5
sides = 24
}
}
drawer.imageFit(image, cell)
}
}
}
}

View File

@@ -0,0 +1,61 @@
package clip
import org.openrndr.application
import org.openrndr.draw.loadImage
import org.openrndr.extra.imageFit.imageFit
import org.openrndr.extra.shadestyles.fills.FillFit
import org.openrndr.extra.shadestyles.fills.clip.clip
import org.openrndr.extra.shapes.primitives.grid
import org.openrndr.extra.shapes.primitives.placeIn
import org.openrndr.math.Vector2
import org.openrndr.math.transforms.transform
import kotlin.math.PI
import kotlin.math.cos
fun main() = application {
configure {
width = 720
height = 720
}
program {
var gf = 0.0
mouse.buttonDown.listen {
gf = 0.1 - gf
}
val image = loadImage("demo-data/images/image-001.png")
extend {
val grid = drawer.bounds.grid(3, 3)
for ((index, cell) in grid.flatten().withIndex()) {
drawer.shadeStyle = clip {
clipFit = FillFit.entries[index/3]
feather = gf
clipTransform = transform {
translate(Vector2(0.5, 0.5))
rotate(36.0 * seconds)
translate(Vector2(-0.5, -0.5))
}
star {
radius = 0.5
center = Vector2(0.5, 0.5)
sharpness = cos( 2 * PI * index / 9.0 + seconds) * 0.25 + 0.5
sides = 24
}
}
val acell = when(val i = index%3) {
1 -> cell.sub(0.0..0.5, 0.0..1.0)
2 -> cell.sub(0.0..1.0, 0.0..0.5)
else -> cell
}
drawer.imageFit(image, acell.placeIn(cell))
}
}
}
}

View File

@@ -0,0 +1,59 @@
package clip
import org.openrndr.application
import org.openrndr.draw.loadImage
import org.openrndr.extra.imageFit.imageFit
import org.openrndr.extra.shadestyles.fills.FillFit
import org.openrndr.extra.shadestyles.fills.clip.clip
import org.openrndr.extra.shapes.primitives.grid
import org.openrndr.extra.shapes.primitives.placeIn
import org.openrndr.math.Vector2
import org.openrndr.math.transforms.transform
import kotlin.math.PI
import kotlin.math.cos
fun main() = application {
configure {
width = 720
height = 720
}
program {
var gf = 0.0
mouse.buttonDown.listen {
gf = 0.1 - gf
}
val image = loadImage("demo-data/images/image-001.png")
extend {
val grid = drawer.bounds.grid(3, 3)
for ((index, cell) in grid.flatten().withIndex()) {
drawer.shadeStyle = clip {
clipFit = FillFit.entries[index/3]
feather = gf
clipTransform = transform {
translate(Vector2(0.5, 0.5))
rotate(36.0 * seconds)
translate(Vector2(-0.5, -0.5))
}
ellipse {
radiusX = 0.5
radiusY = 0.25
}
}
val acell = when(val i = index%3) {
1 -> cell.sub(0.0..0.5, 0.0..1.0)
2 -> cell.sub(0.0..1.0, 0.0..0.5)
else -> cell
}
drawer.imageFit(image, acell.placeIn(cell))
}
}
}
}