[orx-shade-styles] Add pattern shade styles

This commit is contained in:
Edwin Jakobs
2025-03-05 14:57:31 +01:00
parent 3661f5c82a
commit 331c28e52e
6 changed files with 433 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ kotlin {
implementation(project(":orx-noise"))
implementation(project(":orx-shapes"))
implementation(project(":orx-image-fit"))
implementation(project(":orx-camera"))
}
}
}

View File

@@ -0,0 +1,81 @@
package org.openrndr.extra.shadestyles.fills.patterns
import org.openrndr.draw.ShadeStyle
class PatternBaseStructure(
val patternFunction: String,
val domainWarpFunction: String,
)
class PatternBase(structure: PatternBaseStructure) : ShadeStyle() {
var patternUnits: Int by Parameter()
var patternFit: Int by Parameter()
init {
fragmentPreamble = """
${structure.domainWarpFunction}
${structure.patternFunction}
""".trimIndent()
fragmentTransform = """vec2 coord = vec2(0.0);
if (p_patternUnits == 0) { // BOUNDS
coord = c_boundsPosition.xy;
if (p_patternFit == 1) { // COVER
float mx = max(c_boundsSize.x, c_boundsSize.y);
float ar = min(c_boundsSize.x, c_boundsSize.y) / mx;
if (c_boundsSize.x == mx) {
coord.y = (coord.y - 0.5) * ar + 0.5;
} else {
coord.x = (coord.x - 0.5) * ar + 0.5;
}
} else if (p_patternFit == 2) { // CONTAIN
float mx = max(c_boundsSize.x, c_boundsSize.y);
float ar = mx / min(c_boundsSize.x, c_boundsSize.y);
if (c_boundsSize.y == mx) {
coord.y = (coord.y - 0.5) * ar + 0.5;
} else {
coord.x = (coord.x - 0.5) * ar + 0.5;
}
}
} else if (p_patternUnits == 1) { // WORLD
coord = v_worldPosition.xy;
} else if (p_patternUnits == 2) { // VIEW
coord = v_viewPosition.xy;
} else if (p_patternUnits == 3) { // SCREEN
coord = c_screenPosition.xy;
coord.y = u_viewDimensions.y - coord.y;
}
vec2 dx = dFdx(coord);
vec2 dy = dFdy(coord);
int window = p_patternFilterWindow;
float filterSpread = p_patternFilterSpread;
float mask = 0.0;
for (int v = 0; v < window; v++) {
for (int u = 0; u < window; u++) {
float fv = filterSpread * float(v) / (float(window) - 1.0) - 0.5;
float fu = filterSpread * float(u) / (float(window) - 1.0) - 0.5;
vec2 scoord = coord + dx * fu + dy * fv;
vec2 wcoord = patternDomainWarp(scoord);
wcoord = (p_patternTransform * vec4(wcoord, 0.0, 1.0)).xy;
mask += clamp(pattern(wcoord * p_patternScale), 0.0, 1.0);
}
}
mask /= (float(window) * float(window));
if (p_patternInvert) {
mask = 1.0 - mask;
}
vec4 color = mix(p_patternBackgroundColor, p_patternForegroundColor, mask);
x_fill *= color;
x_stroke *= color;
""".trimIndent()
}
}

View File

@@ -0,0 +1,199 @@
package org.openrndr.extra.shadestyles.fills.patterns
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.ObservableHashmap
import org.openrndr.draw.StyleParameters
import org.openrndr.extra.shadestyles.fills.FillFit
import org.openrndr.extra.shadestyles.fills.FillUnits
import org.openrndr.math.Matrix44
class PatternBuilder : StyleParameters {
override var parameterTypes: ObservableHashmap<String, String> = ObservableHashmap(mutableMapOf()) {}
override var parameterValues: MutableMap<String, Any> = mutableMapOf()
override var textureBaseIndex: Int = 2
var filterWindow: Int by Parameter("patternFilterWindow", 5)
var filterSpread: Double by Parameter("patternFilterSpread", 1.0)
var foregroundColor: ColorRGBa by Parameter("patternForegroundColor", ColorRGBa.BLACK)
var backgroundColor: ColorRGBa by Parameter("patternBackgroundColor", ColorRGBa.WHITE)
var patternUnits: FillUnits = FillUnits.BOUNDS
var patternFit: FillFit = FillFit.STRETCH
var patternFunction = """float pattern(vec2 coord) { return 1.0; }"""
var domainWarpFunction = """vec2 patternDomainWarp(vec2 coord) { return coord; }"""
var patternTransform: Matrix44 by Parameter("patternTransform", Matrix44.IDENTITY)
var invert: Boolean by Parameter("patternInvert", false)
var scale: Double by Parameter("patternScale", 1.0)
fun build(): PatternBase {
val structure = PatternBaseStructure(patternFunction, domainWarpFunction)
val patternBase = PatternBase(structure)
patternBase.parameterTypes.putAll(parameterTypes)
patternBase.parameterValues.putAll(parameterValues)
patternBase.patternUnits = patternUnits.ordinal
patternBase.patternFit = patternFit.ordinal
return patternBase
}
/**
* Configures and applies the checkers pattern to the current pattern builder.
*
* @param builder The lambda that defines customization for the CheckerPatternBuilder.
*/
fun checkers(builder: CheckerPatternBuilder.() -> Unit) {
val checkerBuilder = CheckerPatternBuilder(this)
checkerBuilder.builder()
}
/**
* Configures and applies the XOR Modulation pattern to the current pattern builder.
*
* @param builder A lambda scope that defines customization for the XorModPatternBuilder.
*/
fun xorMod(builder: XorModPatternBuilder.() -> Unit) {
val xorModBuilder = XorModPatternBuilder(this)
xorModBuilder.builder()
}
/**
* Configures and applies the XOR Modulation 2 pattern to the current pattern builder.
*
* @param builder A lambda scope that defines customization for the XorMod2PatternBuilder.
*/
fun xorMod2(builder: XorMod2PatternBuilder.() -> Unit) {
val xorModBuilder = XorMod2PatternBuilder(this)
xorModBuilder.builder()
}
/**
* Configures and applies the dots pattern to the current pattern builder.
*
* @param builder A lambda scope that defines customization for the DotsPatternBuilder.
*/
fun dots(builder: DotsPatternBuilder.() -> Unit) {
val dotsPatternBuilder = DotsPatternBuilder(this)
dotsPatternBuilder.builder()
}
/**
* Configures and applies the boxes pattern to the current pattern builder.
*
* @param builder A lambda scope that defines customization for the BoxPatternBuilder.
*/
fun boxes(builder: BoxPatternBuilder.() -> Unit) {
val boxPatternBuilder = BoxPatternBuilder(this)
boxPatternBuilder.builder()
}
/**
* Configures and applies the crosses pattern to the current pattern builder.
*
* @param builder A lambda scope that defines customization for the CrossPatternBuilder.
*/
fun crosses(builder: CrossPatternBuilder.() -> Unit) {
val crossPatternBuilder = CrossPatternBuilder(this)
crossPatternBuilder.builder()
}
}
class CheckerPatternBuilder(builder: PatternBuilder) {
init {
builder.patternFunction = """float pattern(vec2 coord) { return mod(floor(coord.x)+floor(coord.y), 2.0);}"""
}
}
class XorModPatternBuilder(builder: PatternBuilder) {
var patternMod: Int by builder.Parameter("patternMod", 9)
var patternMask: Int by builder.Parameter("patternMask", 3)
init {
builder.patternFunction = """float pattern(vec2 coord) {
ivec2 icoord = ivec2(floor(coord * p_patternScale));
int i = ((icoord.x ^ icoord.y) % p_patternMod) & p_patternMask;
return i == 0 ? 0.0 : 1.0;
}""".trimIndent()
}
}
class XorMod2PatternBuilder(builder: PatternBuilder) {
var patternMod: Int by builder.Parameter("patternMod", 9)
var patternMask: Int by builder.Parameter("patternMask", 3)
var patternOffset: Int by builder.Parameter("patternOffset", 0)
init {
builder.patternFunction = """float pattern(vec2 coord) {
ivec2 icoord = ivec2(floor(coord * p_patternScale));
int i = (icoord.x + icoord.y) ^ (icoord.x - icoord.y);
int i3 = i * i * i;
int i6 = i3 * i3;
int i7 = i * i6;
return (( (i7 + p_patternOffset) % int(p_patternMod)) & int(p_patternMask)) == 0 ? 0.0 : 1.0;
}""".trimIndent()
}
}
class DotsPatternBuilder(builder: PatternBuilder) {
var dotSize: Double by builder.Parameter("patternDotSize", 0.25)
var strokeWeight: Double by builder.Parameter("patternStrokeWeight", 1E10)
init {
builder.patternFunction = """float pattern(vec2 coord) {
vec2 scoord = coord * p_patternScale;
vec2 mcoord = mod(scoord + vec2(0.5), vec2(1.0)) - vec2(0.5);
float d = length(mcoord) - p_patternDotSize;
float dw = fwidth(d);
return smoothstep(dw/2.0, -dw/2.0, d) * smoothstep(-dw/2.0, dw/2.0, d+p_patternStrokeWeight);;
}""".trimIndent()
}
}
class BoxPatternBuilder(builder: PatternBuilder) {
var width: Double by builder.Parameter("patternBoxWidth", 0.5)
var height: Double by builder.Parameter("patternBoxHeight", 0.5)
var rounding: Double by builder.Parameter("patternBoxRounding", 0.0)
var rotation: Double by builder.Parameter("patternBoxRotation", 0.0)
var strokeWeight: Double by builder.Parameter("patternStrokeWeight", 1E10)
init {
builder.patternFunction = """float pattern(vec2 coord) {
float phi = p_patternBoxRotation / 180.0 * 3.141592654;
mat2 rm = mat2(cos(phi), sin(phi), -sin(phi), cos(phi));
vec2 mcoord = mod(coord * p_patternScale + vec2(0.5), vec2(1.0)) - vec2(0.5);
mcoord = rm * mcoord;
vec2 d2 = abs(mcoord) - vec2(p_patternBoxWidth - p_patternBoxRounding, p_patternBoxHeight - p_patternBoxRounding)*0.5;
float d = length(max(d2,0.0)) + min(max(d2.x,d2.y),0.0) - p_patternBoxRounding;
float dw = fwidth(d);
return smoothstep(dw/2.0, -dw/2.0, d) * smoothstep(-dw/2.0, dw/2.0, d+p_patternStrokeWeight);;
}""".trimIndent()
}
}
class CrossPatternBuilder(builder: PatternBuilder) {
var width: Double by builder.Parameter("patternCrossWidth", 0.5)
var weight: Double by builder.Parameter("patternCrossRounding", 0.1)
var rotation: Double by builder.Parameter("patternCrossRotation", 0.0)
var strokeWeight: Double by builder.Parameter("patternStrokeWeight", 1E10)
init {
builder.patternFunction = """float pattern(vec2 coord) {
float phi = p_patternCrossRotation / 180.0 * 3.141592654;
mat2 rm = mat2(cos(phi), sin(phi), -sin(phi), cos(phi));
vec2 mcoord = mod(coord * p_patternScale + vec2(0.5), vec2(1.0)) - vec2(0.5);
mcoord = rm * mcoord;
vec2 p = abs(mcoord);
float d = length(p-min(p.x+p.y, p_patternCrossWidth)*0.5) - p_patternCrossRounding;
float dw = fwidth(d);
return smoothstep(dw/2.0, -dw/2.0, d) * smoothstep(-dw/2.0, dw/2.0, d+p_patternStrokeWeight);;
}""".trimIndent()
}
}
/**
* Creates and returns a new `PatternBase` instance configured using the provided builder function.
*
* @param builder A lambda that operates on a `PatternBuilder` instance to configure the pattern's properties.
* @return A `PatternBase` instance, representing the configured pattern with the applied settings.
*/
fun pattern(builder: PatternBuilder.() -> Unit): PatternBase {
val patternBuilder = PatternBuilder()
patternBuilder.builder()
return patternBuilder.build()
}

View File

@@ -0,0 +1,52 @@
package patterns
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.loadFont
import org.openrndr.draw.loadImage
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.color.presets.NAVY
import org.openrndr.extra.imageFit.imageFit
import org.openrndr.extra.shadestyles.fills.FillUnits
import org.openrndr.extra.shadestyles.fills.patterns.pattern
fun main() = application {
configure {
width = 720
height = 720
}
program {
extend(Camera2D())
val image = loadImage("demo-data/images/image-001.png")
extend {
drawer.shadeStyle = pattern {
backgroundColor = ColorRGBa.NAVY
foregroundColor = ColorRGBa.WHITE
patternUnits = FillUnits.WORLD
parameter("time", seconds*0.1)
// domainWarpFunction = """vec2 patternDomainWarp(vec2 uv) { return uv + vec2(cos(uv.y * 0.1 + p_time), sin(uv.x * 0.1 + p_time)) * 30.05; }"""
scale = 0.4
checkers {
}
}
//drawer.rectangle(drawer.bounds)
drawer.imageFit(image, drawer.bounds)
drawer.shadeStyle = pattern {
backgroundColor = ColorRGBa.NAVY
foregroundColor = ColorRGBa.WHITE
patternUnits = FillUnits.WORLD
parameter("time", seconds)
domainWarpFunction = """vec2 patternDomainWarp(vec2 uv) { return uv + vec2(cos(uv.y * 0.1 + p_time), sin(uv.x * 0.1 + p_time)) * 30.05; }"""
scale = 0.2
checkers {
}
}
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 196.0)
drawer.text("Patterns", 10.0, height / 2.0)
//drawer.circle(drawer.bounds.center, 300.0)
}
}
}

View File

@@ -0,0 +1,37 @@
package patterns
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.loadFont
import org.openrndr.draw.loadImage
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.color.presets.NAVY
import org.openrndr.extra.color.presets.PEACH_PUFF
import org.openrndr.extra.imageFit.imageFit
import org.openrndr.extra.shadestyles.fills.FillUnits
import org.openrndr.extra.shadestyles.fills.patterns.pattern
fun main() = application {
configure {
width = 720
height = 720
}
program {
extend(Camera2D())
extend {
drawer.shadeStyle = pattern {
backgroundColor = ColorRGBa.NAVY
foregroundColor = ColorRGBa.PEACH_PUFF
patternUnits = FillUnits.WORLD
parameter("time", seconds*0.1)
scale = 1.0
xorMod2 {
patternMod = 13
patternOffset = (seconds*1).toInt()
patternMask = 1
}
}
drawer.rectangle(drawer.bounds.scaledBy(100.0))
}
}
}

View File

@@ -0,0 +1,63 @@
package patterns
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.loadFont
import org.openrndr.draw.loadImage
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.color.presets.DARK_GRAY
import org.openrndr.extra.color.presets.NAVY
import org.openrndr.extra.color.presets.PEACH_PUFF
import org.openrndr.extra.imageFit.imageFit
import org.openrndr.extra.shadestyles.fills.FillUnits
import org.openrndr.extra.shadestyles.fills.clip.clip
import org.openrndr.extra.shadestyles.fills.gradients.gradient
import org.openrndr.extra.shadestyles.fills.patterns.pattern
import kotlin.math.cos
fun main() = application {
configure {
width = 720
height = 720
}
program {
extend(Camera2D())
extend {
drawer.shadeStyle = pattern {
backgroundColor = ColorRGBa.DARK_GRAY
foregroundColor = ColorRGBa.PEACH_PUFF
patternUnits = FillUnits.WORLD
parameter("time", seconds*0.1)
scale = 0.2
crosses {
width = 1.0
weight = 0.2
rotation = seconds * 45.0
strokeWeight = cos(seconds) * 0.3 + 0.31
}
} + gradient<ColorRGBa> {
stops[1.0] = ColorRGBa.BLACK
stops[0.5] = ColorRGBa.WHITE
stops[0.0] = ColorRGBa.WHITE
conic { }
} + clip {
star {
sides = 36
sharpness = 0.1
clipOuter = 0.05
clipInner = -0.1
radius = 0.4
}
}
drawer.rectangle(drawer.bounds.offsetEdges(-50.0))
// drawer.fill = ColorRGBa.WHITE
// drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 196.0)
// drawer.text("Patterns", 10.0, height / 2.0)
}
}
}