diff --git a/orx-shade-styles/build.gradle.kts b/orx-shade-styles/build.gradle.kts index e03478fb..07c8b24c 100644 --- a/orx-shade-styles/build.gradle.kts +++ b/orx-shade-styles/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { implementation(project(":orx-shade-styles")) implementation(project(":orx-noise")) implementation(project(":orx-shapes")) + implementation(project(":orx-image-fit")) } } } diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/EllipticalGradient.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/EllipticalGradient.kt new file mode 100644 index 00000000..e22f5173 --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/EllipticalGradient.kt @@ -0,0 +1,112 @@ +package org.openrndr.extra.shadestyles.fills.gradients + +import org.openrndr.color.AlgebraicColor +import org.openrndr.color.ConvertibleToColorRGBa +import org.openrndr.extra.shaderphrases.sdf.sdEllipsePhrase +import org.openrndr.math.CastableToVector4 +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector4 +import kotlin.reflect.KClass + +class EllipticalGradient( + colorType: KClass, + center: Vector2 = Vector2(0.5, 0.5), + radiusX: Double = 1.0, + radiusY: Double = 1.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 radiusX: Double by Parameter() + var radiusY: Double by Parameter() + var center: Vector2 by Parameter() + + init { + this.radiusX = radiusX + this.radiusY = radiusY + this.center = center + } + + companion object { + val gradientFunction = """$sdEllipsePhrase + float gradientFunction(vec2 coord) { + if (abs(p_radiusX - p_radiusY) < 1E-4) { + vec2 d0 = coord - p_center; + float d0l = length(d0); + float f = d0l / p_radiusX; + return f; + } else { + float maxRadius = min(p_radiusX, p_radiusY); + vec2 d0 = (coord - p_center) / maxRadius; + float f = sdEllipse(d0, vec2(p_radiusX, p_radiusY)/maxRadius ); + float f0 = sdEllipse(vec2(0.0), vec2(p_radiusX, p_radiusY)/maxRadius ); + f -= f0; + return f; + } + } + """.trimIndent() + } +} + +class EllipticalGradientBuilder(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 radiusX = 0.5 + var radiusY = 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 EllipticalGradient( + gradientBuilder.colorType, + center, + radiusX, + radiusY, + colors, + stops, + gradientBuilder.structure() + ) + } +} diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBase.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBase.kt index 7fce1239..d37cc1cd 100644 --- a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBase.kt +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBase.kt @@ -93,26 +93,40 @@ open class GradientBase( f = levelWarp(coord, f); if (p_quantization != 0) { - f = floor(f * float(p_quantization)) / (float(p_quantization) - 1.0); + f *= float(p_quantization); + float seam = ceil(f + 0.5); + vec2 d = vec2(dFdx(f), dFdy(f)); + f = (f - seam) / length(d*1.0) + seam; + f = clamp(f, seam-.5, seam+.5); + f /= float(p_quantization); } + float sf; + float mf = 0.0; if (p_spreadMethod == 0) { // PAD - f = clamp(f, 0.0, 1.0); + sf = 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)); + sf = 2.0 * abs(f / 2.0 - floor(f / 2.0 + 0.5)); } else if (p_spreadMethod == 2) { // REPEAT - f = mod(f, 1.0); + sf = mod(f, 1.0); + float seam = ceil(f); + vec2 d = vec2(dFdx(f), dFdy(f)); + mf = (f - seam) / length(d) + seam; } int i = 0; - while (i < p_points_SIZE - 1 && f >= p_points[i+1]) { i++; } + while (i < p_points_SIZE - 1 && sf >= p_points[i+1]) { i++; } vec4 color0 = p_colors[i]; - vec4 color1 = p_colors[i+1]; + vec4 color1 = p_colors[(i+1) % p_colors_SIZE]; - float g = (f - p_points[i]) / (p_points[i+1] - p_points[i]); + float g = (sf - p_points[i]) / (p_points[i+1] - p_points[i]); vec4 gradient = mix(color0, color1, clamp(g, 0.0, 1.0)); + if (mf > 0.0 && p_quantization == 0) { + gradient = p_colors[p_colors_SIZE - 1] * (1.0 - mf) + p_colors[0] * mf; + } + ${generateColorTransform(colorType)} x_fill *= gradient; """ diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBuilder.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBuilder.kt index 97041fd6..c756736d 100644 --- a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBuilder.kt +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/GradientBuilder.kt @@ -57,6 +57,11 @@ class GradientBuilder(val colorType: KClass) gradientFunction = RadialGradient.gradientFunction } + fun elliptic(builder: EllipticalGradientBuilder.() -> Unit) { + shadeStyleBuilder = EllipticalGradientBuilder(this).apply { builder() } + gradientFunction = EllipticalGradient.gradientFunction + } + /** * Configures a conic gradient by applying the provided builder block. * diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/StellarGradient.kt b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/StellarGradient.kt index f86d8315..eab4ab23 100644 --- a/orx-shade-styles/src/commonMain/kotlin/fills/gradients/StellarGradient.kt +++ b/orx-shade-styles/src/commonMain/kotlin/fills/gradients/StellarGradient.kt @@ -2,6 +2,7 @@ package org.openrndr.extra.shadestyles.fills.gradients import org.openrndr.color.AlgebraicColor import org.openrndr.color.ConvertibleToColorRGBa +import org.openrndr.extra.shaderphrases.sdf.sdStarPhrase import org.openrndr.math.CastableToVector4 import org.openrndr.math.Vector2 import org.openrndr.math.Vector4 @@ -42,48 +43,17 @@ open class StellarGradient( } 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() + val gradientFunction = """$sdStarPhrase + float gradientFunction(vec2 coord) { + vec2 d0 = coord - p_center; + d0 = rotate2D(d0, p_rotation); + float f = sdStar(d0 / p_radius, 1.0, p_sides, p_sharpness); + float f0 = sdStar(vec2(0.0), 1.0, p_sides, p_sharpness); + f -= f0; + f /= 0.5 * 1.0 * (1.0 + cos($PI / float(p_sides))); + return f; + } + """.trimIndent() } } diff --git a/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient05.kt b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient05.kt new file mode 100644 index 00000000..cae26b13 --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient05.kt @@ -0,0 +1,46 @@ +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.BLACK + stops[1.0] = ColorRGBa.WHITE + + fillUnits = FillUnits.WORLD + spreadMethod = SpreadMethod.REPEAT + //levelWarpFunction = """float levelWarp(vec2 p, float level) { return level + cos(p.x*0.01 + level)*0.1; } """ + + //quantization = 3 +// stellar { +// radius = drawer.bounds.width/8.0 +// center = drawer.bounds.position(0.5, 0.0) +// sides = 6 +// sharpness = 0.5 +// rotation = seconds * 36.0 +// } + conic { + angle = 360.0 * 8.0 + center = drawer.bounds.position(0.5, 0.5) + } + } + + drawer.rectangle(drawer.bounds) + + } + } + } +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient06.kt b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient06.kt new file mode 100644 index 00000000..98863684 --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/gradients/DemoGradient06.kt @@ -0,0 +1,45 @@ +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 +import org.openrndr.extra.shapes.primitives.grid +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +fun main() = application { + configure { + width = 720 + height = 720 + } + program { + extend { + val grid = drawer.bounds.grid(2, 2) + drawer.stroke = null + + for ((index, cell) in grid.flatten().withIndex()) { + drawer.shadeStyle = gradient { + stops[0.0] = ColorRGBa.RED + stops[0.5] = ColorRGBa.PINK + stops[1.0] = ColorRGBa.WHITE + + fillUnits = FillUnits.BOUNDS + spreadMethod = SpreadMethod.REPEAT + quantization = 8 + elliptic { + radiusX = cos(index / 2.0 * PI + seconds) * 0.45 + 0.5 + radiusY = sin(index / 2.0 * PI + seconds) * 0.45 + 0.5 + } + } + + drawer.rectangle(cell) + + } + } + } +} \ No newline at end of file