diff --git a/orx-color/src/commonMain/kotlin/phrases/Phrases.kt b/orx-color/src/commonMain/kotlin/phrases/Phrases.kt index c8a94d67..7177158e 100644 --- a/orx-color/src/commonMain/kotlin/phrases/Phrases.kt +++ b/orx-color/src/commonMain/kotlin/phrases/Phrases.kt @@ -3,14 +3,27 @@ package org.openrndr.extra.color.phrases import org.openrndr.extra.shaderphrases.ShaderPhrase import org.openrndr.extra.shaderphrases.ShaderPhraseBook -object ColorPhraseBook : ShaderPhraseBook("color") { - val phraseAtan2 = ShaderPhrase(""" - |float atan2(in float y, in float x) { - | bool s = (abs(x) > abs(y)); - | return mix(PI/2.0 - atan(x,y), atan(y,x), float(s)); - |}""".trimMargin()) +val oklabToLinearRgbPhrase = """#ifndef ORX_COLOR_OKLAB_TO_LINEAR_RGB_PHRASE + |#define ORX_COLOR_OKLAB_TO_LINEAR_RGB_PHRASE + |vec4 oklab_to_linear_rgb(vec4 lab) { + | const mat3 kLMStoCONE = mat3( + | 1.0, 1.0, 1.0, + | 0.3963377774, -0.1055613458, -0.0894841775, + | 0.2158037573, -0.0638541728, -1.2914855480); + | const mat3 kRot = mat3( + | 4.0767416621, -1.2684380046, -0.0041960863, + | -3.3077115913, 2.6097574011, -0.7034186147, + | 0.2309699292, -0.3413193965, 1.7076147010); + | vec3 lms = kLMStoCONE * lab.rgb; + | lms = lms * lms * lms; + | vec4 res = vec4(kRot * lms,lab.a); + | return res; + |} + |#endif""".trimMargin() - val phraseLinearRgbToOKLab = ShaderPhrase(""" +val linearRgbToOklabPhrase = """ + |#ifndef ORX_COLOR_LINEAR_RGB_TO_OKLAB_PHRASE + |#define ORX_COLOR_LINEAR_RGB_TO_OKLAB_PHRASE |vec4 linear_rgb_to_oklab(vec4 c) { | c.rgb = max(vec3(0.0), c.rgb); | const mat3 kCONEtoLMS = mat3( @@ -25,23 +38,19 @@ object ColorPhraseBook : ShaderPhraseBook("color") { | vec3 lms = pow(kCONEtoLMS * c.rgb, vec3(1.0/3.0)); | vec4 res = vec4((kRot) * lms, c.a); | return res; + |} + |#endif""".trimMargin() + +object ColorPhraseBook : ShaderPhraseBook("color") { + val phraseAtan2 = ShaderPhrase(""" + |float atan2(in float y, in float x) { + | bool s = (abs(x) > abs(y)); + | return mix(PI/2.0 - atan(x,y), atan(y,x), float(s)); |}""".trimMargin()) - val oklabToLinearRgb = ShaderPhrase(""" - |vec4 oklab_to_linear_rgb(vec4 lab) { - | const mat3 kLMStoCONE = mat3( - | 1.0, 1.0, 1.0, - | 0.3963377774, -0.1055613458, -0.0894841775, - | 0.2158037573, -0.0638541728, -1.2914855480); - | const mat3 kRot = mat3( - | 4.0767416621, -1.2684380046, -0.0041960863, - | -3.3077115913, 2.6097574011, -0.7034186147, - | 0.2309699292, -0.3413193965, 1.7076147010); - | vec3 lms = kLMStoCONE * lab.rgb; - | lms = lms * lms * lms; - | vec4 res = vec4(kRot * lms,lab.a); - | return res; - |}""".trimMargin()) + val phraseLinearRgbToOKLab = ShaderPhrase(linearRgbToOklabPhrase) + + val oklabToLinearRgb = ShaderPhrase(oklabToLinearRgbPhrase) val phraseLabToLch = ShaderPhrase( """ |vec4 lab_to_lch(vec4 lab) { diff --git a/orx-shade-styles/src/commonMain/kotlin/AngularGradient.kt b/orx-shade-styles/src/commonMain/kotlin/AngularGradient.kt index 93981cec..5cdba018 100644 --- a/orx-shade-styles/src/commonMain/kotlin/AngularGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/AngularGradient.kt @@ -9,6 +9,7 @@ import org.openrndr.extra.parameters.Description import org.openrndr.extra.parameters.DoubleParameter import org.openrndr.math.Vector2 +@Deprecated("use gradient {} instead") @Description("Angular gradient") class AngularGradient( color0: ColorRGBa, @@ -62,6 +63,7 @@ class AngularGradient( } } +@Deprecated("use gradient {} instead") fun angularGradient( color0: ColorRGBa, color1: ColorRGBa, diff --git a/orx-shade-styles/src/commonMain/kotlin/ColorspaceHelper.kt b/orx-shade-styles/src/commonMain/kotlin/ColorspaceHelper.kt index 82349955..ee159118 100644 --- a/orx-shade-styles/src/commonMain/kotlin/ColorspaceHelper.kt +++ b/orx-shade-styles/src/commonMain/kotlin/ColorspaceHelper.kt @@ -7,7 +7,7 @@ import kotlin.reflect.KClass internal fun generateColorTransform(kClass: KClass<*>): String { return when (kClass) { ColorRGBa::class -> """""" - ColorOKLABa::class -> """gradient = linear_rgb_to_srgb(oklab_to_linear_rgb(gradient));""" + ColorOKLABa::class -> """gradient = oklab_to_linear_rgb(gradient);""" else -> error("color space not supported $kClass") } } \ No newline at end of file diff --git a/orx-shade-styles/src/commonMain/kotlin/HalfAngularGradient.kt b/orx-shade-styles/src/commonMain/kotlin/HalfAngularGradient.kt index 76747b1b..1a1529ec 100644 --- a/orx-shade-styles/src/commonMain/kotlin/HalfAngularGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/HalfAngularGradient.kt @@ -9,6 +9,7 @@ import org.openrndr.extra.parameters.Description import org.openrndr.extra.parameters.DoubleParameter import org.openrndr.math.Vector2 +@Deprecated("use gradient {} instead") @Description("Half-angular gradient") class HalfAngularGradient( color0: ColorRGBa, diff --git a/orx-shade-styles/src/commonMain/kotlin/ImageFit.kt b/orx-shade-styles/src/commonMain/kotlin/ImageFit.kt deleted file mode 100644 index 17ea3d4d..00000000 --- a/orx-shade-styles/src/commonMain/kotlin/ImageFit.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.openrndr.extra.shadestyles - -import org.openrndr.draw.ColorBuffer -import org.openrndr.draw.ShadeStyle -import org.openrndr.math.Vector2 - -class ImageFit : ShadeStyle() { - - var image: ColorBuffer by Parameter() - var flipV: Boolean by Parameter() - var position: Vector2 by Parameter() - - init { - position = Vector2.ZERO - fragmentTransform = """ - | vec2 uv = c_boundsPosition.xy; - | vec2 ts = textureSize(p_image, 0); - | float boundsAR = c_boundsSize.x / c_boundsSize.y; - | vec2 shift = (p_position + vec2(1.0, 1.0)) / 2.0; - | - | if (c_boundsSize.x > c_boundsSize.y) { - | uv.y -= shift.y; - | uv.y /= boundsAR; - | uv.y += shift.y; - | } else { - | uv.x -= shift.x; - | uv.x *= boundsAR; - | uv.x += shift.x; - | } - | float textureAR = ts.x / ts.y; - | if (ts.x > ts.y) { - | uv.x -= 0.5; - | uv.x /= textureAR; - | uv.x += 0.5; - | } else { - | uv.y -= 0.5; - | uv.y *= textureAR; - | uv.y += 0.5; - | } - | - | - | if (p_flipV) { - | uv.y = 1.0 - uv.y; - | } - | #ifndef OR_GL_TEXTURE2D - | vec4 img = texture(p_image, uv); - | #else - | vec4 img = texture2D(p_image, uv); - | #endif - | x_fill = img; - | """.trimMargin() - } -} - -fun imageFit(image: ColorBuffer, position: Vector2 = Vector2.ZERO) : ImageFit { - val im = ImageFit() - im.image = image - im.flipV = true - im.position = position - return im -} diff --git a/orx-shade-styles/src/commonMain/kotlin/LinearGradient.kt b/orx-shade-styles/src/commonMain/kotlin/LinearGradient.kt index a144ac1e..2ff8d0fc 100644 --- a/orx-shade-styles/src/commonMain/kotlin/LinearGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/LinearGradient.kt @@ -14,6 +14,7 @@ import org.openrndr.extra.shaderphrases.preprocess import org.openrndr.math.CastableToVector4 import org.openrndr.math.Vector2 +@Deprecated("use gradient {} instead") @Description("Linear gradient") open class LinearGradientBase( color0: C, @@ -79,6 +80,7 @@ open class LinearGradientBase( } } +@Deprecated("use gradient {} instead") class LinearGradient( color0: ColorRGBa = ColorRGBa.BLACK, color1: ColorRGBa = ColorRGBa.WHITE, @@ -87,6 +89,7 @@ class LinearGradient( exponent: Double = 1.0 ): LinearGradientBase(color0, color1, offset, rotation, exponent) +@Deprecated("use gradient {} instead") class LinearGradientOKLab( color0: ColorOKLABa, color1: ColorOKLABa, @@ -96,6 +99,7 @@ class LinearGradientOKLab( ): LinearGradientBase(color0, color1, offset, rotation, exponent) +@Deprecated("use gradient {} instead") fun linearGradient( color0: ColorRGBa = ColorRGBa.BLACK, color1: ColorRGBa = ColorRGBa.WHITE, @@ -106,6 +110,7 @@ fun linearGradient( return LinearGradient(color0, color1, offset, rotation, exponent) } +@Deprecated("use gradient {} instead") fun linearGradient( color0: ColorOKLABa = ColorRGBa.BLACK.toOKLABa(), color1: ColorOKLABa = ColorRGBa.WHITE.toOKLABa(), diff --git a/orx-shade-styles/src/commonMain/kotlin/NPointGradient.kt b/orx-shade-styles/src/commonMain/kotlin/NPointGradient.kt index 814e2e01..04768f38 100644 --- a/orx-shade-styles/src/commonMain/kotlin/NPointGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/NPointGradient.kt @@ -7,6 +7,7 @@ import org.openrndr.draw.ShadeStyle import org.openrndr.extra.parameters.Description import org.openrndr.math.Vector2 +@Deprecated("use gradient {} instead") @Description("N-Point gradient") class NPointGradient( colors: Array, diff --git a/orx-shade-styles/src/commonMain/kotlin/NPointLinearGradient.kt b/orx-shade-styles/src/commonMain/kotlin/NPointLinearGradient.kt index 1ebf51b3..23d2ff16 100644 --- a/orx-shade-styles/src/commonMain/kotlin/NPointLinearGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/NPointLinearGradient.kt @@ -13,6 +13,7 @@ import org.openrndr.extra.color.spaces.ColorOKLABa import org.openrndr.math.CastableToVector4 import org.openrndr.math.Vector2 +@Deprecated("use gradient {} instead") @Description("Multicolor linear gradient") open class NPointLinearGradientBase( colors: Array, diff --git a/orx-shade-styles/src/commonMain/kotlin/NPointRadialGradient.kt b/orx-shade-styles/src/commonMain/kotlin/NPointRadialGradient.kt index 2e74893d..9a3737b5 100644 --- a/orx-shade-styles/src/commonMain/kotlin/NPointRadialGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/NPointRadialGradient.kt @@ -7,6 +7,7 @@ import org.openrndr.draw.ShadeStyle import org.openrndr.extra.parameters.Description import org.openrndr.math.Vector2 +@Deprecated("use gradient {} instead") @Description("Multicolor radial gradient") class NPointRadialGradient( colors: Array, diff --git a/orx-shade-styles/src/commonMain/kotlin/RadialGradient.kt b/orx-shade-styles/src/commonMain/kotlin/RadialGradient.kt index c75ff9dc..8fc08c1f 100644 --- a/orx-shade-styles/src/commonMain/kotlin/RadialGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/RadialGradient.kt @@ -15,6 +15,7 @@ import org.openrndr.extra.shaderphrases.preprocess import org.openrndr.math.CastableToVector4 import org.openrndr.math.Vector2 +@Deprecated("use gradient {} instead") @Description("Radial gradient") open class RadialGradientBase( color0: C, @@ -69,6 +70,7 @@ where C : ConvertibleToColorRGBa, C : AlgebraicColor, C: CastableToVector4 { } } +@Deprecated("use gradient {} instead") class RadialGradient( color0: ColorRGBa, color1: ColorRGBa, @@ -78,6 +80,7 @@ class RadialGradient( exponent: Double = 1.0 ): RadialGradientBase(color0, color1, offset, rotation, length, exponent) +@Deprecated("use gradient {} instead") class RadialGradientOKLab( color0: ColorOKLABa, color1: ColorOKLABa, @@ -87,6 +90,7 @@ class RadialGradientOKLab( exponent: Double = 1.0 ): RadialGradientBase(color0, color1, offset, rotation, length, exponent) +@Deprecated("use gradient {} instead") fun radialGradient( color0: ColorRGBa, color1: ColorRGBa, @@ -98,6 +102,7 @@ fun radialGradient( return RadialGradient(color0, color1, offset, rotation, length, exponent) } +@Deprecated("use gradient {} instead") fun radialGradient( color0: ColorOKLABa, color1: ColorOKLABa, diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/FillEnums.kt b/orx-shade-styles/src/commonMain/kotlin/fills/FillEnums.kt new file mode 100644 index 00000000..37c7151d --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/FillEnums.kt @@ -0,0 +1,20 @@ +package org.openrndr.extra.shadestyles.fills + +enum class FillFit { + STRETCH, + COVER, + CONTAIN +} + +enum class FillUnits { + BOUNDS, + WORLD, + VIEW, + SCREEN, +} + +enum class SpreadMethod { + PAD, + REFLECT, + REPEAT +} diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/ConicGradient.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/ConicGradient.kt new file mode 100644 index 00000000..4839baeb --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/ConicGradient.kt @@ -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( + colorType: KClass, + center: Vector2 = Vector2(0.5, 0.5), + rotation: Double = 0.0, + angle: Double = 0.0, + startAngle: Double = 0.0, + colors: Array, + points: Array = Array(colors.size) { it / (colors.size - 1.0) }, + structure: GradientBaseStructure +) : GradientBase( + colorType, + colors, + points, + structure +) + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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(private val gradientBuilder: GradientBuilder) : GradientShadeStyleBuilder + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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 { + val (stops, colors) = gradientBuilder.extractStepsUnzip() + return ConicGradient( + gradientBuilder.colorType, + center, + rotation, + angle, + startAngle, + colors, + stops, + gradientBuilder.structure() + ) + } +} diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBuilder.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBuilder.kt new file mode 100644 index 00000000..97041fd6 --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBuilder.kt @@ -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(val colorType: KClass) + where C : ConvertibleToColorRGBa, C : AlgebraicColor, C : CastableToVector4 { + + var stops = mutableMapOf() + 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) { + style.quantization = quantization + style.spreadMethod = spreadMethod.ordinal + style.fillUnits = fillUnits.ordinal + style.fillFit = fillFit.ordinal + } + + @PublishedApi + internal var shadeStyleBuilder: GradientShadeStyleBuilder = 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.() -> 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.() -> 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.() -> 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.() -> Unit) { + shadeStyleBuilder = StellarGradientBuilder(this).apply { builder() } + gradientFunction = StellarGradient.gradientFunction + } + + internal fun extractSteps(): List> { + return stops.entries.sortedBy { it.key }.map { + Pair(it.key, it.value) + } + } + + internal fun extractStepsUnzip(): Pair, Array> { + 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 { + return this.shadeStyleBuilder.build().apply { + setBaseParameters(this) + } + } +} + +sealed interface GradientShadeStyleBuilder + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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 +} + + +/** + * 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 gradient(builder: GradientBuilder.() -> Unit): ShadeStyle + where C : ConvertibleToColorRGBa, C : AlgebraicColor, C : CastableToVector4 { + val gb = GradientBuilder(C::class) + gb.builder() + return gb.build() +} \ No newline at end of file diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/LinearGradient.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/LinearGradient.kt new file mode 100644 index 00000000..1fc50d67 --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/LinearGradient.kt @@ -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( + colorType: KClass, + colors: Array, + points: Array = Array(colors.size) { it / (colors.size - 1.0) }, + structure: GradientBaseStructure +) : ShadeStyle() + where C : ConvertibleToColorRGBa, C : AlgebraicColor, C : CastableToVector4 { + + var quantization: Int by Parameter() + var colors: Array by Parameter() + var points: Array 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( + colorType: KClass, + start: Vector2 = Vector2.ZERO, + end: Vector2 = Vector2.ONE, + colors: Array, + points: Array = Array(colors.size) { it / (colors.size - 1.0) }, + structure: GradientBaseStructure, + ) : GradientBase( + colorType, + colors, + points, + structure +) + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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(private val gradientBuilder: GradientBuilder) : GradientShadeStyleBuilder + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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 { + val (stops, colors) = gradientBuilder.extractStepsUnzip() + return LinearGradient(gradientBuilder.colorType, start, end, colors, stops, gradientBuilder.structure()) + } +} diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/RadialGradient.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/RadialGradient.kt new file mode 100644 index 00000000..4f1bcfe9 --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/RadialGradient.kt @@ -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( + colorType: KClass, + center: Vector2 = Vector2(0.5, 0.5), + focalCenter: Vector2 = center, + radius: Double = 1.0, + focalRadius: Double = radius, + colors: Array, + points: Array = Array(colors.size) { it / (colors.size - 1.0) }, + structure: GradientBaseStructure +) : GradientBase( + colorType, + colors, + points, + structure +) + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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(private val gradientBuilder: GradientBuilder) : GradientShadeStyleBuilder + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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 { + val (stops, colors) = gradientBuilder.extractStepsUnzip() + return RadialGradient( + gradientBuilder.colorType, + center, + focalCenter ?: center, + radius, + focalRadius ?: radius, + colors, + stops, + gradientBuilder.structure() + ) + } +} diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/StellarGradient.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/StellarGradient.kt new file mode 100644 index 00000000..f86d8315 --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/StellarGradient.kt @@ -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( + colorType: KClass, + center: Vector2 = Vector2(0.5, 0.5), + radius: Double = 1.0, + sharpness: Double = 0.5, + rotation: Double = 0.0, + sides: Int = 3, + colors: Array, + points: Array = Array(colors.size) { it / (colors.size - 1.0) }, + structure: GradientBaseStructure + +) : GradientBase( + colorType, + colors, + points, + structure +) + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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(private val gradientBuilder: GradientBuilder) : GradientShadeStyleBuilder + where C : ConvertibleToColorRGBa, C : AlgebraicColor, 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 { + val (stops, colors) = gradientBuilder.extractStepsUnzip() + return StellarGradient( + gradientBuilder.colorType, + center, + radius, + sharpness, + rotation, + sides, + colors, + stops, + gradientBuilder.structure() + ) + } +} diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/image/ImageFill.kt b/orx-shade-styles/src/commonMain/kotlin/fills/image/ImageFill.kt new file mode 100644 index 00000000..26f81401 --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/image/ImageFill.kt @@ -0,0 +1,274 @@ +package org.openrndr.extra.shadestyles.fills.image + +import org.openrndr.draw.ColorBuffer +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.Matrix44 +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +import org.openrndr.math.Vector4 + +class ImageFillBuilder { + /** + * Specifies the units in which the image fill is defined. + * + * The `fillUnits` property determines how the dimensions of the fill area + * are interpreted when applying an image to a shape or area. It supports + * several predefined units of measure, as defined in the `FillUnits` enumeration: + * - `BOUNDS`: Interprets the fill area in the context of the bounding box of the shape. + * - `WORLD`: Uses world coordinates to define the fill area. + * - `VIEW`: Aligns the fill area to the view space. + * - `SCREEN`: Defines the fill area based on screen coordinates. + * + * The default value is `FillUnits.BOUNDS`. + */ + var fillUnits = FillUnits.BOUNDS + + + /** + * Specifies how the image should fit within the bounds of the target shape or area. + * + * This property determines the scaling and alignment behavior when applying an image as + * a fill. The options available are defined in the `FillFit` enum: + * + * - `STRETCH`: Stretches the image to completely fill the bounds, potentially distorting + * its aspect ratio. + * - `COVER`: Scales the image to cover the entire bounds while maintaining its + * aspect ratio. Portions of the image that exceed the bounds are cropped. + * - `CONTAIN`: Scales the image to fit entirely within the bounds while maintaining + * its aspect ratio, leaving empty space if the aspect ratio differs. + * + * The default value for this property is `FillFit.COVER`. + */ + var fillFit = FillFit.COVER + + /** + * Specifies the method used to determine how the edges of the image fill are handled along the x-axis. + * The value is of type [SpreadMethod], and it defines how pixel values are repeated or reflected + * when they exceed the bounds of the original image. + * By default, it is set to [SpreadMethod.PAD], which extends the edge pixels of the image. + */ + var spreadMethodX = SpreadMethod.PAD + + /** + * Specifies the method used to determine how the edges of the image fill are handled along the y-axis. + * The value is of type [SpreadMethod], allowing for pixel values to be extended, reflected, or repeated + * when exceeding the bounds of the image. By default, it is set to [SpreadMethod.PAD], which extends the + * edge pixels of the image. + */ + var spreadMethodY = SpreadMethod.PAD + + + /** + * Represents an optional `ColorBuffer` used as an input image for image-based fill operations. + * This property is used in conjunction with various fill attributes (e.g., `fillUnits`, `fillFit`, + * `spreadMethodX`, `spreadMethodY`, `fillTransform`) to define how the image is applied to a shape. + * + * If this property is not set, an error will be thrown during the build process of the `ImageFill`. + * + * The image may be modified via parameters like flipping vertically (`flipV`), scaling, + * or transforming. It plays a central role in defining visual aspects of a gradient or + * custom fill style. + */ + var image: ColorBuffer? = null + + + /** + * A transformation matrix applied to the image fill. This property allows modifying the spatial + * characteristics of the image used in the fill, such as translation, rotation, or scaling. + * Using this matrix, the image can be adjusted to achieve specific visual effects or alignment + * within the fill bounds. By default, it is set to the identity matrix, which means no transformation + * is applied. + */ + var fillTransform: Matrix44 = Matrix44.IDENTITY + + + /** + * Customizable GLSL function used to perform domain warping on a given vector. + * This property allows defining a function written in GLSL that alters the texture coordinate `p` + * for advanced procedural texturing techniques or creative transformations. + * + * The default value is a no-op function that directly returns the input vector: + * `vec2 if_domainWarp(vec2 p) { return p; }`. + * + * This property can be dynamically updated to specify a custom domain warp, such as displacements + * based on time, noise, or other parameters. + * + * To apply domain warping, update this property with a valid GLSL function that operates on `vec2` input + * and returns a transformed `vec2` coordinate. + */ + var domainWarpFunction: String = """vec2 if_domainWarp(vec2 p) { return p; }""" + + + /** + * A mutable map that stores custom parameter key-value pairs to configure the behavior of the image fill. + * + * Keys should be strings representing parameter names, while values can be objects of supported types including: + * - `Double` + * - `Int` + * - `Vector2` + * - `Vector3` + * - `Vector4` + * - `Matrix44` + * + * This map is primarily used to pass additional configuration data to the domain warp function or other + * dynamic aspects of the image fill. Unsupported value types will result in an error. + */ + var parameters = mutableMapOf() + + var scale = 1.0 + fun build(): ImageFill { + val imageFill = ImageFill(domainWarpFunction) + imageFill.if_image = image ?: error("image not set") + imageFill.if_fillUnits = fillUnits.ordinal + imageFill.if_fillFit = fillFit.ordinal + imageFill.if_spreadMethodX = spreadMethodX.ordinal + imageFill.if_spreadMethodY = spreadMethodY.ordinal + imageFill.if_flipV = image?.flipV ?: false + imageFill.if_scale = scale + imageFill.if_fillTransform = fillTransform + for ((key, value) in parameters) { + when (value) { + is Double -> imageFill.parameter(key, value) + is Int -> imageFill.parameter(key, value) + is Vector2 -> imageFill.parameter(key, value) + is Vector3 -> imageFill.parameter(key, value) + is Vector4 -> imageFill.parameter(key, value) + is Matrix44 -> imageFill.parameter(key, value) + else -> error("unsupported type $key $value") + } + } + + return imageFill + } +} + +/** + * Provides shaded rendering using an image as the fill for a shape or geometry. + * + * The `ImageFill` class enables customization of how an image is applied as a fill, + * providing properties to control image source, scaling, positioning, and orientation. + * This is achieved through shader-based rendering, with configurable parameters for + * precise control over the resulting visual representation. + * + * Image behavior is determined by the following properties: + * - `image`: The image to be used as the fill, represented as a `ColorBuffer`. + * - `flipV`: A flag to vertically flip the image. This is useful when the vertical + * orientation of the original image needs correction. + * - `position`: A `Vector2` specifying the offset positioning for the image within + * the bounds of the shape. + * - `useScreenBounds`: When enabled, adjusts the image mapping to use the screen + * space bounds rather than the shape's bounds. + * + * The rendering logic dynamically adjusts the image's aspect ratio and alignment, + * maintaining proportional scaling between the image and target bounds. This ensures + * correct rendering for shapes of varying sizes or orientations. + */ +class ImageFill(domainWarpFunction: String = """vec2 if_domainWarp(vec2 p) { return p; }""") : ShadeStyle() { + var if_image: ColorBuffer by Parameter() + var if_flipV: Boolean by Parameter() + var if_position: Vector2 by Parameter() + var if_fillFit: Int by Parameter() + var if_fillUnits: Int by Parameter() + var if_spreadMethodX: Int by Parameter() + var if_spreadMethodY: Int by Parameter() + var if_scale: Double by Parameter() + var if_fillTransform: Matrix44 by Parameter() + + init { + if_fillTransform = Matrix44.IDENTITY + if_fillFit = FillFit.COVER.ordinal + if_fillUnits = FillUnits.BOUNDS.ordinal + if_spreadMethodX = SpreadMethod.PAD.ordinal + if_spreadMethodY = SpreadMethod.PAD.ordinal + if_position = Vector2.ZERO + if_scale = 1.0 + fragmentPreamble = """$domainWarpFunction""" + fragmentTransform = """ + | vec2 ts = vec2(textureSize(p_if_image, 0)); + | vec2 uv; // = c_boundsPosition.xy; + | vec2 boundsSize; // = c_boundsSize.xy; + | + | if (p_if_fillUnits == 0) { // BOUNDS + | uv = c_boundsPosition.xy; + | boundsSize = c_boundsSize.xy; + | } else if (p_if_fillUnits == 1) { // WORLD + | boundsSize = vec2(textureSize(p_if_image, 0)); + | uv = v_worldPosition.xy / boundsSize; + | } + | + | float boundsAR = boundsSize.x / boundsSize.y; + | vec2 shift = (p_if_position + vec2(1.0, 1.0)) / 2.0; + | + | if (boundsSize.x >boundsSize.y) { + | uv.y -= shift.y; + | uv.y /= boundsAR; + | uv.y += shift.y; + | } else { + | uv.x -= shift.x; + | uv.x *= boundsAR; + | uv.x += shift.x; + | } + | uv = if_domainWarp(uv); + | uv = (p_if_fillTransform * vec4(uv, 0.0, 1.0)).xy; + | uv -= vec2(0.5); + | uv /= p_if_scale; + | uv += vec2(0.5); + | float textureAR = ts.x / ts.y; + | if (ts.x > ts.y) { + | uv.x -= 0.5; + | uv.x /= textureAR; + | uv.x += 0.5; + | } else { + | uv.y -= 0.5; + | uv.y *= textureAR; + + | uv.y += 0.5; + | } + | float alphaMask = 1.0; + | if (p_if_spreadMethodX == 0) { // PAD + | alphaMask *= uv.x >= 0.0 && uv.x < 1.0 ? 1.0 : 0.0; + | uv.x = clamp(uv.x, 0.0, 1.0); + | } else if (p_if_spreadMethodX == 1) { // REFLECT + | uv.x = 2.0 * abs(uv.x / 2.0 - floor(uv.x / 2.0 + 0.5)); + | } else if (p_if_spreadMethodX == 2) { // REPEAT + | uv.x = mod(uv.x, 1.0); + | } + | if (p_if_spreadMethodY == 0) { // PAD + | alphaMask *= uv.y >= 0.0 && uv.y < 1.0 ? 1.0 : 0.0; + | uv.y = clamp(uv.y, 0.0, 1.0); + | } else if (p_if_spreadMethodY == 1) { // REFLECT + | uv.y = 2.0 * abs(uv.y / 2.0 - floor(uv.y / 2.0 + 0.5)); + | } else if (p_if_spreadMethodY == 2) { // REPEAT + | uv.y = mod(uv.y, 1.0); + | } + | + | if (!p_if_flipV) { + | uv.y = 1.0 - uv.y; + | } + | + | vec4 img = texture(p_if_image, uv); + | img.a *= alphaMask; + | x_fill *= img; + | """.trimMargin() + } +} + + +/** + * Creates an ImageFill instance using the provided configuration. + * + * The method allows for the configuration of image fill options such as scaling, + * transformations, spread methods, and more by applying the specified configuration + * to an internal ImageFillBuilder. + * + * @param builder A lambda that configures the properties of the ImageFillBuilder + * instance used to construct the ImageFill object. + * @return An ImageFill object configured based on the input builder settings. + */ +fun imageFill(builder: ImageFillBuilder.() -> Unit): ImageFill { + val im = ImageFillBuilder().apply(builder).build() + return im +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/DemoAllGradients01.kt b/orx-shade-styles/src/jvmDemo/kotlin/DemoAllGradients01.kt deleted file mode 100644 index 14b0b94c..00000000 --- a/orx-shade-styles/src/jvmDemo/kotlin/DemoAllGradients01.kt +++ /dev/null @@ -1,89 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorRGBa -import org.openrndr.draw.isolated -import org.openrndr.extra.shadestyles.* -import org.openrndr.math.Polar -import org.openrndr.shape.Rectangle - -/** - * Example of 5 gradient styles. - * NPointLinear and NPoingGradient have separate demos. - */ -fun main() = application { - configure { - width = 1000 - height = 500 - } - program { - // Create gradients with initial colors - val gradients = listOf( - RadialGradient(ColorRGBa.PINK, ColorRGBa.WHITE), - AngularGradient(ColorRGBa.PINK, ColorRGBa.WHITE), - NPointGradient(Array(4) { - ColorRGBa.PINK.shade(it / 3.0) - }), - LinearGradient(ColorRGBa.PINK, ColorRGBa.WHITE), - HalfAngularGradient(ColorRGBa.PINK, ColorRGBa.WHITE) - ) - - extend { - gradients.forEachIndexed { gradientId, gradient -> - for (column in 0 until 10) { - val color1 = ColorRGBa.PINK.toHSVa().shiftHue(column * 12.0) - .shade(0.5).toRGBa() - - val w = width.toDouble() / 10.0 - val h = height.toDouble() / gradients.size - val rect = Rectangle(column * w, gradientId * h, w, h) - - val offset = Polar((seconds + column) * 15.0, 0.3).cartesian - - drawer.isolated { - when (gradient) { - is RadialGradient -> { - gradient.color1 = color1 - gradient.exponent = column / 3.0 + 0.3 - gradient.length = 0.6 - gradient.offset = offset - } - - is AngularGradient -> { - gradient.color1 = color1 - gradient.exponent = column / 3.0 + 0.3 - gradient.rotation = (seconds - column) * 10.0 - gradient.offset = offset - } - - is LinearGradient -> { - gradient.color1 = color1 - gradient.exponent = column / 3.0 + 0.3 - gradient.rotation = seconds * 10.0 - } - - is HalfAngularGradient -> { - gradient.color1 = color1 - gradient.exponent = column / 3.0 + 0.3 - gradient.rotation = (column - seconds) * 10.0 - gradient.offset = offset - } - - is NPointGradient -> { - // Animate points. - // We could also animate colors. - gradient.points = Array(gradient.colors.size) { - rect.center + Polar( - it * 90.0 + - column * 36 - seconds * 10, - 40.0 - ).cartesian - } - } - } - shadeStyle = gradient - rectangle(rect) - } - } - } - } - } -} diff --git a/orx-shade-styles/src/jvmDemo/kotlin/DemoImageFit01.kt b/orx-shade-styles/src/jvmDemo/kotlin/DemoImageFit01.kt deleted file mode 100644 index b988af22..00000000 --- a/orx-shade-styles/src/jvmDemo/kotlin/DemoImageFit01.kt +++ /dev/null @@ -1,23 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorRGBa -import org.openrndr.draw.loadImage -import org.openrndr.extra.shadestyles.imageFit -import org.openrndr.extra.shadestyles.linearGradient -import org.openrndr.math.Vector2 -import org.openrndr.shape.Circle -import kotlin.math.cos -import kotlin.math.sin - -fun main() = application { - program { - val image = loadImage("demo-data/images/image-001.png") - extend { - drawer.shadeStyle = imageFit(image, Vector2(cos(seconds), sin(seconds))) + linearGradient(ColorRGBa.RED, ColorRGBa.BLUE) - drawer.circle(drawer.bounds.center, 200.0) - drawer.rectangle(10.0, 10.0, 400.0, 50.0) - drawer.rectangle(10.0, 10.0, 50.0, 400.0) - drawer.contour(Circle(width/2.0, height/2.0, 50.0).contour) - } - } -} - diff --git a/orx-shade-styles/src/jvmDemo/kotlin/DemoLinearGradient.kt b/orx-shade-styles/src/jvmDemo/kotlin/DemoLinearGradient.kt deleted file mode 100644 index 727f0353..00000000 --- a/orx-shade-styles/src/jvmDemo/kotlin/DemoLinearGradient.kt +++ /dev/null @@ -1,23 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorRGBa -import org.openrndr.extra.color.spaces.toOKLABa -import org.openrndr.extra.shadestyles.linearGradient - -fun main() = application { - program { - extend { - drawer.shadeStyle = linearGradient( - ColorRGBa.RED.toOKLABa(), - ColorRGBa.BLUE.toOKLABa(), - ) - drawer.rectangle(120.0, 40.0, 200.0, 400.0) - - drawer.shadeStyle = linearGradient( - ColorRGBa.RED, - ColorRGBa.BLUE - ) - - drawer.rectangle(120.0 + 200.0, 40.0, 200.0, 400.0) - } - } -} diff --git a/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointGradient01.kt b/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointGradient01.kt deleted file mode 100644 index 2cf8dbab..00000000 --- a/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointGradient01.kt +++ /dev/null @@ -1,44 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorRGBa -import org.openrndr.color.ColorXSVa -import org.openrndr.extra.shadestyles.NPointGradient -import org.openrndr.math.Polar -import org.openrndr.shape.ShapeContour -import kotlin.math.PI -import kotlin.math.cos - -/** - * Demonstrate using an n-point gradient. - * The gradient has 8 points in screen coordinates and 8 colors. - * The colors are fixed, the points of the gradient move. - * A contour is drawn using the same points from the gradient, - * but note that this is not necessary: you can animate the gradient - * on a static shape (a circle for example) or you can animate a shape - * with a static gradient. - */ -fun main() = application { - program { - val numPoints = 8 - val gradient = NPointGradient(Array(numPoints) { - ColorXSVa(it * 360.0 / numPoints, 1.0, 1.0).toRGBa() - }) - - extend { - drawer.run { - clear(ColorRGBa.WHITE.shade(0.9)) - val t = PI * 2 * (frameCount % 300) / 300.0 - val points = Array(numPoints) { - val lfo = cos(it * PI / 2 - t) - val theta = it * 360.0 / numPoints - 22.5 * lfo - val radius = 200 + 170 * lfo - bounds.center + Polar(theta, radius).cartesian - } - gradient.points = points - shadeStyle = gradient - stroke = ColorRGBa.WHITE - strokeWeight = 4.0 - contour(ShapeContour.fromPoints(points.asList(), true)) - } - } - } -} diff --git a/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointLinearGradient01.kt b/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointLinearGradient01.kt deleted file mode 100644 index b1d94508..00000000 --- a/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointLinearGradient01.kt +++ /dev/null @@ -1,64 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorXSVa -import org.openrndr.color.rgb -import org.openrndr.extra.color.spaces.toOKLABa -import org.openrndr.extra.shadestyles.NPointLinearGradient -import org.openrndr.extra.shadestyles.NPointLinearGradientOKLab -import org.openrndr.shape.Rectangle -import kotlin.math.pow -import kotlin.math.sin - -/** - * Demonstrate using a multicolor linear gradient. - * The gradient has 8 static saturated colors. - * The positions of the colors are first distributed - * uniformly between 0.0 and 1.0 and then animated towards one of - * the ends over time using pow() and sin(seconds). - */ -fun main() = application { - program { - val numPoints = 8 - // Create gradients using two different color spaces - val gradients = listOf( - NPointLinearGradient(Array(numPoints) { - ColorXSVa(it * 360.0 / numPoints, 1.0, 1.0).toRGBa() - }), - // OKLab is better at maintaining luminosity across the gradient - NPointLinearGradientOKLab(Array(numPoints) { - ColorXSVa(it * 360.0 / numPoints, 1.0, 1.0).toRGBa() - .toOKLABa() - }) - ) - - extend { - // The points should be sorted values between 0.0 and 1.0 - val distribution = Array(numPoints) { - // uniform distribution - // (it / (numPoints - 1.0)) - - // skewed and animated distribution - (it / (numPoints - 1.0)).pow(1.0 + 0.5 * sin(seconds)) - } - drawer.run { - clear(rgb(0.2)) - stroke = rgb(0.35) - strokeWeight = 8.0 - - gradients.forEachIndexed { i, gradient -> - shadeStyle = gradient - gradient.points = distribution - - gradient.rotation = seconds * 10 - circle(bounds.position(0.34, 0.29 + 0.44 * i), 110.0) - - gradient.rotation += 90 - rectangle( - Rectangle.fromCenter( - bounds.position(0.655, 0.29 + 0.44 * i), 200.0 - ) - ) - } - } - } - } -} diff --git a/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointRadialGradient01.kt b/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointRadialGradient01.kt deleted file mode 100644 index f0e536f5..00000000 --- a/orx-shade-styles/src/jvmDemo/kotlin/DemoNPointRadialGradient01.kt +++ /dev/null @@ -1,42 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorRGBa -import org.openrndr.color.rgb -import org.openrndr.extra.shadestyles.NPointRadialGradient -import org.openrndr.shape.Circle -import kotlin.random.Random - -/** - * Demonstrate using a multicolor radial gradient. - * The gradient has 5 colors (first and last ones are transparent). - * Any of the properties can be animated, including colors and points. - * See DemoNPointLinearGradient01.kt for an example of animated properties. - */ -fun main() = application { - program { - val gradient = NPointRadialGradient( - arrayOf( - ColorRGBa.PINK.opacify(0.0), - ColorRGBa.PINK, ColorRGBa.WHITE, ColorRGBa.PINK, - ColorRGBa.PINK.opacify(0.0) - ), arrayOf(0.0, 0.4, 0.5, 0.6, 1.0) - ) - - val circles = List(25) { - Circle( - Random.nextDouble() * drawer.width, - Random.nextDouble() * drawer.height, - Random.nextDouble() * 150.0 - ) - } - - extend { - drawer.run { - clear(rgb(0.2)) - shadeStyle = gradient - fill = ColorRGBa.WHITE - stroke = null - circles(circles) - } - } - } -} diff --git a/orx-shade-styles/src/jvmDemo/kotlin/DemoRadialGradient01.kt b/orx-shade-styles/src/jvmDemo/kotlin/DemoRadialGradient01.kt deleted file mode 100644 index 57d4ee5e..00000000 --- a/orx-shade-styles/src/jvmDemo/kotlin/DemoRadialGradient01.kt +++ /dev/null @@ -1,17 +0,0 @@ -import org.openrndr.application -import org.openrndr.color.ColorRGBa -import org.openrndr.extra.shadestyles.radialGradient -import kotlin.math.cos - -fun main() = application { - program { - extend { - drawer.shadeStyle = radialGradient( - ColorRGBa.PINK, - ColorRGBa.PINK.toHSVa().shiftHue(180.0).shade(0.5).toRGBa(), - exponent = cos(seconds) * 0.5 + 0.5 - ) - drawer.rectangle(120.0, 40.0, 400.0, 400.0) - } - } -} diff --git a/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient01.kt b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient01.kt new file mode 100644 index 00000000..31b61c0e --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient01.kt @@ -0,0 +1,80 @@ +package gradients + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.shadestyles.fills.SpreadMethod +import org.openrndr.extra.shadestyles.fills.gradients.gradient +import org.openrndr.math.Vector2 +import kotlin.math.cos + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + extend { + drawer.shadeStyle = gradient { + stops[0.0] = ColorRGBa.RED + stops[0.1] = ColorRGBa.GREEN + stops[0.2] = ColorRGBa.PINK + stops[0.9] = ColorRGBa.WHITE + stops[1.0] = ColorRGBa.BLACK + + linear { + start = Vector2(0.1, 0.1).rotate(seconds * 36.0, Vector2(0.5, 0.5)) + end = Vector2(0.9, 0.9).rotate(seconds * 36.0, Vector2(0.5, 0.5)) + + } + } + drawer.rectangle(0.0, 0.0, 360.0, 360.0) + + + drawer.shadeStyle = gradient { + stops[0.0] = ColorRGBa.RED + stops[0.1] = ColorRGBa.GREEN + stops[0.2] = ColorRGBa.PINK + stops[0.9] = ColorRGBa.WHITE + stops[1.0] = ColorRGBa.BLACK + spreadMethod = SpreadMethod.REFLECT + stellar { + radius = (cos(seconds) * 0.25 + 0.5) * 0.5 + sharpness = 0.5 + sides = 6 + rotation = seconds * 36.0 + } + } + drawer.rectangle(360.0, 0.0, 360.0, 360.0) + + drawer.shadeStyle = gradient { + stops[0.0] = ColorRGBa.RED + stops[0.1] = ColorRGBa.GREEN + stops[0.2] = ColorRGBa.PINK + stops[0.9] = ColorRGBa.WHITE + stops[1.0] = ColorRGBa.BLACK + spreadMethod = SpreadMethod.REFLECT + radial { + radius = (cos(seconds) * 0.25 + 0.5) * 0.5 + } + } + drawer.rectangle(360.0, 360.0, 360.0, 360.0) + + + drawer.shadeStyle = gradient { + stops[0.0] = ColorRGBa.RED + stops[0.1] = ColorRGBa.GREEN + stops[0.2] = ColorRGBa.PINK + stops[0.9] = ColorRGBa.WHITE + stops[1.0] = ColorRGBa.BLACK + spreadMethod = SpreadMethod.REPEAT + linear { + start = Vector2(0.45, 0.45).rotate(seconds * 36.0) + end = Vector2(0.55, 0.55).rotate(seconds * 36.0) + } + } + drawer.rectangle(0.0, 360.0, 360.0, 360.0) + } + } + } +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient02.kt b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient02.kt new file mode 100644 index 00000000..6e46ea94 --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient02.kt @@ -0,0 +1,64 @@ +package gradients + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.loadFont +import org.openrndr.extra.color.presets.BLUE_STEEL +import org.openrndr.extra.shadestyles.fills.FillUnits +import org.openrndr.extra.shadestyles.fills.SpreadMethod +import org.openrndr.extra.shadestyles.fills.gradients.gradient + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + extend { + drawer.shadeStyle = gradient { + stops[0.0] = ColorRGBa.BLUE_STEEL + stops[0.75] = ColorRGBa.WHITE + stops[0.8] = ColorRGBa.BLACK + + quantization = 10 + fillUnits = FillUnits.WORLD + spreadMethod = SpreadMethod.REFLECT + levelWarpFunction = """float levelWarp(vec2 p, float level) { return level + cos(p.x*0.01 + level)*0.1; } """ + + stellar { + radius = drawer.bounds.width/4.0 + center = drawer.bounds.position(0.5, 0.0) + sides = 6 + sharpness = 0.5 + rotation = seconds * 36.0 + } + } + + drawer.rectangle(drawer.bounds) + drawer.shadeStyle = gradient { + stops[0.0] = ColorRGBa.BLUE_STEEL + stops[0.75] = ColorRGBa.WHITE + stops[0.8] = ColorRGBa.BLACK + + quantization = 10 + fillUnits = FillUnits.WORLD + spreadMethod = SpreadMethod.REFLECT + levelWarpFunction = """float levelWarp(vec2 p, float level) { return level + 0.1 + cos(p.x*0.01 + level)*0.1; } """ + + stellar { + radius = drawer.bounds.width/4.0 + center = drawer.bounds.position(0.5, 0.0) + sides = 6 + sharpness = 0.5 + rotation = seconds * 36.0 + } + } + drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 196.0) + for (i in 0 until 5) { + drawer.text("Gradient", 0.0, 128.0 + i * drawer.height / 5.0) + } + } + } + } +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient03.kt b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient03.kt new file mode 100644 index 00000000..cae75baf --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient03.kt @@ -0,0 +1,36 @@ +package gradients + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.color.presets.BLUE_STEEL +import org.openrndr.extra.color.spaces.OKHSV +import org.openrndr.extra.color.tools.shiftHue +import org.openrndr.extra.shadestyles.fills.SpreadMethod +import org.openrndr.extra.shadestyles.fills.gradients.gradient +import org.openrndr.math.Vector2 + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + extend { + drawer.shadeStyle = gradient { + for (i in 0 .. 10) { + stops[i/10.0] = ColorRGBa.RED.shiftHue(i * 36.0) + + } + spreadMethod = SpreadMethod.REFLECT + conic { + center = Vector2(0.5, 0.5) + angle = 360.0 + rotation = seconds * 10.0 + } + } + drawer.rectangle(drawer.bounds) + } + } + } +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill01.kt b/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill01.kt new file mode 100644 index 00000000..7bbfe2f6 --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill01.kt @@ -0,0 +1,25 @@ +package image + +import org.openrndr.application +import org.openrndr.draw.loadImage +import org.openrndr.extra.shadestyles.fills.FillUnits +import org.openrndr.extra.shadestyles.fills.SpreadMethod +import org.openrndr.extra.shadestyles.fills.image.imageFill +import org.openrndr.math.transforms.transform + +fun main() = application { + configure { + width = 720 + height = 720 + } + program { + var img = loadImage("demo-data/images/image-001.png") + extend { + drawer.shadeStyle = imageFill { + image = img + } + drawer.circle(drawer.bounds.center, 200.0) + } + } +} + diff --git a/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill02.kt b/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill02.kt new file mode 100644 index 00000000..6e631802 --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill02.kt @@ -0,0 +1,35 @@ +package image + +import org.openrndr.application +import org.openrndr.draw.loadImage +import org.openrndr.extra.shadestyles.fills.FillUnits +import org.openrndr.extra.shadestyles.fills.SpreadMethod +import org.openrndr.extra.shadestyles.fills.image.imageFill +import org.openrndr.math.transforms.transform +import kotlin.math.cos + +fun main() = application { + configure { + width = 720 + height = 720 + } + program { + var img = loadImage("demo-data/images/image-001.png") + extend { + for (i in 0 until 10) { + drawer.shadeStyle = imageFill { + image = img + fillTransform = transform { + translate(0.5, 0.5) + rotate( cos(i * 0.5 + seconds*10.0) *10.0 ) + scale(1.0 - i * 0.05) + translate(-0.5, -0.5) + } + } + //drawer.stroke = null + drawer.circle(drawer.bounds.center, 360.0 - i * 18.0) + } + } + } +} + diff --git a/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill03.kt b/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill03.kt new file mode 100644 index 00000000..06b961cd --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/image/DemoImageFill03.kt @@ -0,0 +1,30 @@ +package image + +import org.openrndr.application +import org.openrndr.draw.loadImage +import org.openrndr.extra.shadestyles.fills.FillUnits +import org.openrndr.extra.shadestyles.fills.SpreadMethod +import org.openrndr.extra.shadestyles.fills.image.imageFill +import org.openrndr.math.transforms.transform +import kotlin.math.cos + +fun main() = application { + configure { + width = 720 + height = 720 + } + program { + var img = loadImage("demo-data/images/image-001.png") + extend { + drawer.shadeStyle = imageFill { + image = img + parameters["time"] = seconds + domainWarpFunction = """vec2 if_domainWarp(vec2 p) { return p + vec2(cos(p.y * 20.0 + p_time), sin(p.x * 20.0 + p_time)) * 0.1; }""" + spreadMethodX = SpreadMethod.REFLECT + spreadMethodY = SpreadMethod.REFLECT + } + drawer.circle(drawer.bounds.center, 360.0) + } + } +} +