[orx-shadestyles] Improve gradient and imageFill shadestyles

This commit is contained in:
Edwin Jakobs
2025-02-25 12:16:57 +01:00
parent 21a3d7f483
commit 9a93d95318
30 changed files with 1296 additions and 386 deletions

View File

@@ -0,0 +1,106 @@
package org.openrndr.extra.shadestyles.fills.gradients
import org.openrndr.color.AlgebraicColor
import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.math.CastableToVector4
import org.openrndr.math.Vector2
import org.openrndr.math.Vector4
import kotlin.math.PI
import kotlin.reflect.KClass
open class ConicGradient<C>(
colorType: KClass<C>,
center: Vector2 = Vector2(0.5, 0.5),
rotation: Double = 0.0,
angle: Double = 0.0,
startAngle: Double = 0.0,
colors: Array<Vector4>,
points: Array<Double> = Array(colors.size) { it / (colors.size - 1.0) },
structure: GradientBaseStructure
) : GradientBase<C>(
colorType,
colors,
points,
structure
)
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
var angle: Double by Parameter()
var startAngle: Double by Parameter()
var center: Vector2 by Parameter()
var rotation: Double by Parameter()
init {
this.center = center
this.startAngle = startAngle
this.angle = angle
this.rotation = rotation
}
companion object {
val gradientFunction = """
float gradientFunction(vec2 coord) {
vec2 d0 = coord - p_center;
float angle = atan(d0.y, d0.x);
angle += ${PI};
angle /= ${2.0 * PI};
angle += p_rotation / 360.0;
angle = mod(angle, 1.0);
angle *= p_angle / 360.0;
angle += $PI * p_startAngle / 180.0;
return angle;
}
""".trimIndent()
}
}
class ConicGradientBuilder<C>(private val gradientBuilder: GradientBuilder<C>) : GradientShadeStyleBuilder<C>
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
/**
* Specifies the center point for the gradient.
* When using BOUNDS coordinates, the coordinates are normalized, where (0,0) represents the top-left corner and (1,1)
* represents the bottom-right corner. The default value is set to `Vector2(0.5, 0.5)`, which corresponds to the center
* of the gradient's bounding box.
*/
var center = Vector2(0.5, 0.5)
/**
* Defines the angular extent of the conic gradient in degrees.
* By default, it is set to 360.0 degrees, representing a full circular gradient.
* Adjusting this value can control the gradient's angular sweep, with values ranging between 0 and 360.
* Negative values or values exceeding 360 might have no effect or be clamped depending on implementation.
*/
var angle: Double = 360.0
/**
* Specifies the starting angle of the conic gradient in degrees.
* This value determines the initial orientation of the gradient's angular sweep.
* By default, it is set to `0.0` degrees, which aligns with a standard reference point.
* You can adjust this value to rotate the gradient's starting position around the center.
*/
var startAngle: Double = 0.0
/**
* Defines the rotation angle of the conic gradient in degrees.
* This value applies a global rotation to the gradient, rotating it around its center point.
* By default, it is set to `0.0` degrees, meaning no rotation is applied.
* Modifying this value allows for tilting the gradient's angular orientation to achieve
* specific visual effects or alignments.
*/
var rotation: Double = 0.0
override fun build(): GradientBase<C> {
val (stops, colors) = gradientBuilder.extractStepsUnzip()
return ConicGradient<C>(
gradientBuilder.colorType,
center,
rotation,
angle,
startAngle,
colors,
stops,
gradientBuilder.structure()
)
}
}

View File

@@ -0,0 +1,146 @@
package org.openrndr.extra.shadestyles.fills.gradients
import org.openrndr.color.AlgebraicColor
import org.openrndr.color.ColorRGBa
import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.draw.ShadeStyle
import org.openrndr.extra.shadestyles.fills.FillFit
import org.openrndr.extra.shadestyles.fills.FillUnits
import org.openrndr.extra.shadestyles.fills.SpreadMethod
import org.openrndr.math.CastableToVector4
import org.openrndr.math.Vector4
import kotlin.reflect.KClass
class GradientBuilder<C>(val colorType: KClass<C>)
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
var stops = mutableMapOf<Double, C>()
var fillUnits = FillUnits.BOUNDS
var fillFit = FillFit.STRETCH
var spreadMethod = SpreadMethod.PAD
var levelWarpFunction = """float levelWarp(vec2 coord, float level) { return level; }"""
var domainWarpFunction = """vec2 domainWarp(vec2 coord) { return coord; }"""
var gradientFunction = """float gradientFunction(vec2 coord) { return 0.0; }"""
var quantization = 0
private fun setBaseParameters(style: GradientBase<C>) {
style.quantization = quantization
style.spreadMethod = spreadMethod.ordinal
style.fillUnits = fillUnits.ordinal
style.fillFit = fillFit.ordinal
}
@PublishedApi
internal var shadeStyleBuilder: GradientShadeStyleBuilder<C> = LinearGradientBuilder(this)
/**
* Configures a linear gradient by applying the provided builder block.
*
* @param builder A lambda function used to define the properties of the linear gradient.
* The builder block allows customization of attributes such as
* start and end positions.
*/
fun linear(builder: LinearGradientBuilder<C>.() -> Unit) {
shadeStyleBuilder = LinearGradientBuilder(this).apply { builder() }
gradientFunction = LinearGradient.gradientFunction
}
/**
* Configures a radial gradient by applying the provided builder block.
*
* @param builder A lambda function used to define the properties of the radial gradient.
* The builder block allows customization of attributes such as the center,
* radius, focal center, and focal radius.
*/
fun radial(builder: RadialGradientBuilder<C>.() -> Unit) {
shadeStyleBuilder = RadialGradientBuilder(this).apply { builder() }
gradientFunction = RadialGradient.gradientFunction
}
/**
* Configures a conic gradient by applying the provided builder block.
*
* @param builder A lambda function used to define the properties of the conic gradient.
* The builder block allows customization of attributes such as the center,
* angle, start angle, and rotation.
*/
fun conic(builder: ConicGradientBuilder<C>.() -> Unit) {
shadeStyleBuilder = ConicGradientBuilder(this).apply { builder() }
gradientFunction = ConicGradient.gradientFunction
}
/**
* Configures a stellar gradient by applying the provided builder block.
*
* @param builder A lambda function used to define the properties of the stellar gradient.
* The builder block allows customization of attributes such as center, radius,
* sharpness, rotation, and the number of sides.
*/
fun stellar(builder: StellarGradientBuilder<C>.() -> Unit) {
shadeStyleBuilder = StellarGradientBuilder(this).apply { builder() }
gradientFunction = StellarGradient.gradientFunction
}
internal fun extractSteps(): List<Pair<Double, C>> {
return stops.entries.sortedBy { it.key }.map {
Pair(it.key, it.value)
}
}
internal fun extractStepsUnzip(): Pair<Array<Double>, Array<Vector4>> {
val steps = extractSteps()
val stopsArray = Array(steps.size) { steps[it].first }
val colorsArray = Array(steps.size) {
(steps[it].second.let { c ->
if (c is ColorRGBa) {
c.toLinear()
} else {
c
}
}).toVector4()
}
return Pair(stopsArray, colorsArray)
}
internal fun structure(): GradientBaseStructure =
GradientBaseStructure(gradientFunction, domainWarpFunction, levelWarpFunction)
@PublishedApi
internal fun build(): GradientBase<C> {
return this.shadeStyleBuilder.build().apply {
setBaseParameters(this)
}
}
}
sealed interface GradientShadeStyleBuilder<C>
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
/**
* Constructs and returns a `GradientBase` object representing a gradient with the
* desired configuration defined in the implementing class.
*
* @return An instance of `GradientBase` configured with the specified gradient parameters.
*/
fun build(): GradientBase<C>
}
/**
* Creates a gradient shade style using the specified configuration.
*
* The method allows for building a gradient using a DSL-like approach,
* where different properties such as gradient stops, gradient type, and
* other configurations can be set.
*
* @param builder A lambda function used to configure the gradient properties
* through an instance of [GradientBuilder].
* @return A [ShadeStyle] instance representing the configured gradient.
*/
inline fun <reified C> gradient(builder: GradientBuilder<C>.() -> Unit): ShadeStyle
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
val gb = GradientBuilder(C::class)
gb.builder()
return gb.build()
}

View File

@@ -0,0 +1,187 @@
package org.openrndr.extra.shadestyles.fills.gradients
import org.openrndr.color.AlgebraicColor
import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.draw.ShadeStyle
import org.openrndr.extra.color.phrases.linearRgbToOklabPhrase
import org.openrndr.extra.color.phrases.oklabToLinearRgbPhrase
import org.openrndr.extra.shadestyles.fills.FillFit
import org.openrndr.extra.shadestyles.fills.FillUnits
import org.openrndr.extra.shadestyles.fills.SpreadMethod
import org.openrndr.extra.shadestyles.generateColorTransform
import org.openrndr.math.CastableToVector4
import org.openrndr.math.Vector2
import org.openrndr.math.Vector4
import kotlin.math.PI
import kotlin.reflect.KClass
class GradientBaseStructure(
val gradientFunction: String,
val domainWarpFunction: String,
val levelWarpFunction: String
)
open class GradientBase<C>(
colorType: KClass<C>,
colors: Array<Vector4>,
points: Array<Double> = Array(colors.size) { it / (colors.size - 1.0) },
structure: GradientBaseStructure
) : ShadeStyle()
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
var quantization: Int by Parameter()
var colors: Array<Vector4> by Parameter()
var points: Array<Double> by Parameter()
var spreadMethod: Int by Parameter()
var fillUnits: Int by Parameter()
var fillFit: Int by Parameter()
init {
this.quantization = 0
this.colors = colors
this.points = points
this.fillUnits = FillUnits.BOUNDS.ordinal
this.spreadMethod = SpreadMethod.PAD.ordinal
this.fillFit = FillFit.STRETCH.ordinal
fragmentPreamble = """
|vec2 rotate2D(vec2 x, float angle){
| float rad = angle / 180.0 * $PI;
| mat2 m = mat2(cos(rad),-sin(rad), sin(rad),cos(rad));
| return m * x;
|}
|$oklabToLinearRgbPhrase
|$linearRgbToOklabPhrase
|${structure.gradientFunction}
|${structure.domainWarpFunction}
|${structure.levelWarpFunction}
|""".trimMargin()
fragmentTransform = """
vec2 coord = vec2(0.0);
if (p_fillUnits == 0) { // BOUNDS
coord = c_boundsPosition.xy;
if (p_fillFit == 1) { // COVER
float mx = max(c_boundsSize.x, c_boundsSize.y);
float ar = mx / min(c_boundsSize.x, c_boundsSize.y);
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_fillFit == 2) { // CONTAIN
float mx = max(c_boundsSize.x, c_boundsSize.y);
float ar = min(c_boundsSize.x, c_boundsSize.y) / mx;
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_fillUnits == 1) { // WORLD
coord = v_worldPosition.xy;
} else if (p_fillUnits == 2) { // VIEW
coord = v_viewPosition.xy;
} else if (p_fillUnits == 3) { // SCREEN
coord = c_screenPosition.xy;
coord.y = u_viewDimensions.y - coord.y;
}
coord = domainWarp(coord);
float f = gradientFunction(coord);
f = levelWarp(coord, f);
if (p_quantization != 0) {
f = floor(f * float(p_quantization)) / (float(p_quantization) - 1.0);
}
if (p_spreadMethod == 0) { // PAD
f = clamp(f, 0.0, 1.0);
} else if (p_spreadMethod == 1) { // REFLECT
f = 2.0 * abs(f / 2.0 - floor(f / 2.0 + 0.5));
} else if (p_spreadMethod == 2) { // REPEAT
f = mod(f, 1.0);
}
int i = 0;
while (i < p_points_SIZE - 1 && f >= p_points[i+1]) { i++; }
vec4 color0 = p_colors[i];
vec4 color1 = p_colors[i+1];
float g = (f - p_points[i]) / (p_points[i+1] - p_points[i]);
vec4 gradient = mix(color0, color1, clamp(g, 0.0, 1.0));
${generateColorTransform(colorType)}
x_fill *= gradient;
"""
}
}
open class LinearGradient<C>(
colorType: KClass<C>,
start: Vector2 = Vector2.ZERO,
end: Vector2 = Vector2.ONE,
colors: Array<Vector4>,
points: Array<Double> = Array(colors.size) { it / (colors.size - 1.0) },
structure: GradientBaseStructure,
) : GradientBase<C>(
colorType,
colors,
points,
structure
)
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
var start: Vector2 by Parameter()
var end: Vector2 by Parameter()
init {
this.start = start
this.end = end
}
companion object {
val gradientFunction = """
float gradientFunction(vec2 coord) {
vec2 d0 = coord - p_start;
vec2 dl = p_end - p_start;
float d0l = length(d0);
float dll = length(dl);
float f = dot(d0, dl) / (dll * dll);
return f;
}
""".trimIndent()
}
}
class LinearGradientBuilder<C>(private val gradientBuilder: GradientBuilder<C>) : GradientShadeStyleBuilder<C>
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
/**
* Specifies the start point of the linear gradient.
* The coordinate values are normalized when using BOUNDS coordinates,
* where (0,0) represents the top-left corner and (1,1) represents the bottom-right corner of the gradient's bounding box.
* The default value is set to `Vector2(0.0, 0.5)`, which places the start point at the left middle edge of the bounding box.
*/
var start = Vector2(0.0, 0.5)
/**
* Specifies the end point of the linear gradient.
* The coordinate values are normalized when using BOUNDS coordinates, where (0,0) represents
* the top-left corner and (1,1) represents the bottom-right corner of the gradient's bounding box.
* The default value is set to `Vector2(1.0, 0.5)`, which places the end point at the right middle edge
* of the bounding box.
*/
var end = Vector2(1.0, 0.5)
override fun build(): GradientBase<C> {
val (stops, colors) = gradientBuilder.extractStepsUnzip()
return LinearGradient<C>(gradientBuilder.colorType, start, end, colors, stops, gradientBuilder.structure())
}
}

View File

@@ -0,0 +1,105 @@
package org.openrndr.extra.shadestyles.fills.gradients
import org.openrndr.color.AlgebraicColor
import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.math.CastableToVector4
import org.openrndr.math.Vector2
import org.openrndr.math.Vector4
import kotlin.reflect.KClass
open class RadialGradient<C>(
colorType: KClass<C>,
center: Vector2 = Vector2(0.5, 0.5),
focalCenter: Vector2 = center,
radius: Double = 1.0,
focalRadius: Double = radius,
colors: Array<Vector4>,
points: Array<Double> = Array(colors.size) { it / (colors.size - 1.0) },
structure: GradientBaseStructure
) : GradientBase<C>(
colorType,
colors,
points,
structure
)
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
var radius: Double by Parameter()
var focalRadius: Double by Parameter()
var focalCenter: Vector2 by Parameter()
var center: Vector2 by Parameter()
init {
this.focalRadius = focalRadius
this.focalCenter = focalCenter
this.center = center
this.radius = radius
}
companion object {
val gradientFunction = """
float gradientFunction(vec2 coord) {
vec2 d0 = coord - p_center;
float d0l = length(d0);
float f = d0l / p_radius;
return f;
}
""".trimIndent()
}
}
class RadialGradientBuilder<C>(private val gradientBuilder: GradientBuilder<C>) : GradientShadeStyleBuilder<C>
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
/**
* Specifies the center point for the radial gradient.
*
* The `center` represents the normalized coordinates within the bounds of the gradient's area.
* When using BOUNDS coordinates a value of `Vector2(0.5, 0.5)` corresponds to the geometric center of the gradient's
* bounds. The coordinates are normalized, where (0,0) is the top-left corner and (1,1) is the bottom-right corner.
* This value determines the starting position for the radial gradient effect.
*/
var center = Vector2(0.5, 0.5)
/**
* Specifies the radius of the radial gradient.
*
* The `radius` determines the extent of the gradient from the center point outward.
*
* When using BOUNDS coordinates it is expressed as a normalized value where `0.0` represents no radius
* (a single point at the center) and `1.0` corresponds to the full extent to the edge of the gradient's bounding area.
* Adjusting this value alters the size and spread of the gradient.
*/
var radius = 0.5
/**
* Specifies the focal center point for the radial gradient.
*
* The `focalCenter` defines an additional center point for the radial gradient,
* allowing for more complex and visually distinct gradient effects compared to the default center.
* If not explicitly set, it defaults to the same value as the `center`.
*
* This property can be used to create focused or offset gradient patterns by positioning
* the focal center differently relative to the main center point. The coordinates can
* be normalized within the bounds, where (0,0) represents the top-left corner and (1,1)
* represents the bottom-right corner.
*/
var focalCenter: Vector2? = null
var focalRadius: Double? = null
override fun build(): GradientBase<C> {
val (stops, colors) = gradientBuilder.extractStepsUnzip()
return RadialGradient(
gradientBuilder.colorType,
center,
focalCenter ?: center,
radius,
focalRadius ?: radius,
colors,
stops,
gradientBuilder.structure()
)
}
}

View File

@@ -0,0 +1,140 @@
package org.openrndr.extra.shadestyles.fills.gradients
import org.openrndr.color.AlgebraicColor
import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.math.CastableToVector4
import org.openrndr.math.Vector2
import org.openrndr.math.Vector4
import kotlin.math.PI
import kotlin.reflect.KClass
open class StellarGradient<C>(
colorType: KClass<C>,
center: Vector2 = Vector2(0.5, 0.5),
radius: Double = 1.0,
sharpness: Double = 0.5,
rotation: Double = 0.0,
sides: Int = 3,
colors: Array<Vector4>,
points: Array<Double> = Array(colors.size) { it / (colors.size - 1.0) },
structure: GradientBaseStructure
) : GradientBase<C>(
colorType,
colors,
points,
structure
)
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
var sides: Int by Parameter()
var radius: Double by Parameter()
var center: Vector2 by Parameter()
var sharpness: Double by Parameter()
var rotation: Double by Parameter()
init {
this.sides = sides
this.center = center
this.radius = radius
this.sharpness = sharpness
this.rotation = rotation
}
companion object {
val gradientFunction = """
const float pi = $PI;
const vec3 c = vec3(1,0,-1);
/*
taken from https://www.shadertoy.com/view/csXcD8# by ShaderToy user 'nr4'
*/
float dstar(in vec2 x, in float r1, in float r2, in float N) {
N *= 2.;
float p = atan(x.y,x.x),
k = pi/N,
dp = mod(p, 2.*k),
parity = mod(round((p-dp)*.5/k), 2.),
dkp = mix(k,-k,parity),
kp = k+dkp,
km = k-dkp;
vec2 p1 = r1*vec2(cos(km),sin(km)),
p2 = r2*vec2(cos(kp),sin(kp)),
dpp = p2-p1,
n = normalize(dpp).yx*c.xz,
xp = length(x)*vec2(cos(dp), sin(dp)),
xp1 = xp-p1;
float t = dot(xp1,dpp)/dot(dpp,dpp)-.5,
r = (1.-2.*parity)*dot(xp1,n);
return t < -.5
? sign(r)*length(xp1)
: t < .5
? r
: sign(r)*length(xp-p2);
}
float gradientFunction(vec2 coord) {
vec2 d0 = coord - p_center;
d0 = rotate2D(d0, p_rotation);
float innerRadius = 1.0 - p_sharpness;
float f = dstar(d0 / p_radius, innerRadius, 1.0, float(p_sides));
// dstar is broken at vec2(0.0, 0.0), let's nudge it a bit
float f0 = dstar(vec2(1E-6, 1E-6), innerRadius, 1.0, float(p_sides));
f -= f0;
f /= 0.5 * 1.0 * (1.0 + cos(pi / float(p_sides)));
return f;
}
""".trimIndent()
}
}
class StellarGradientBuilder<C>(private val gradientBuilder: GradientBuilder<C>) : GradientShadeStyleBuilder<C>
where C : ConvertibleToColorRGBa, C : AlgebraicColor<C>, C : CastableToVector4 {
/**
* Specifies the center point of the gradient.
* The center is defined in normalized coordinates where (0, 0) represents the top-left corner
* and (1, 1) represents the bottom-right corner of the gradient's bounding box.
* The default value is `Vector2(0.5, 0.5)`, which corresponds to the center of the gradient.
*/
var center = Vector2(0.5, 0.5)
var radius = 0.5
/**
* Specifies the number of sides for the star pattern used in the gradient.
* This property controls the symmetry and appearance of the resulting gradient.
* Higher values produce shapes with more sides, contributing to more intricate patterns,
* while lower values result in simpler, less detailed designs.
* The default value is set to `6`.
*/
var sides = 6
/**
* Determines the sharpness of the star shape. Maximum value is `1.0` which will produce pointy stars.
* Values closer to `0.0` result in smoother, star shapes. A value of `0.0` will result in a regular polygon shape.
* The default value is `0.5`.
*/
var sharpness = 0.5
/**
* Specifies the rotation angle of the gradient in degrees.
* This property adjusts the overall rotation of the gradient around its center point.
* By default, the value is set to `0.0` degrees, indicating no rotation.
* Modifying this value allows the gradient's orientation to be tilted, enabling various aesthetic effects.
*/
var rotation = 0.0
override fun build(): GradientBase<C> {
val (stops, colors) = gradientBuilder.extractStepsUnzip()
return StellarGradient(
gradientBuilder.colorType,
center,
radius,
sharpness,
rotation,
sides,
colors,
stops,
gradientBuilder.structure()
)
}
}