diff --git a/orx-shade-styles/build.gradle.kts b/orx-shade-styles/build.gradle.kts index 07c8b24c..7dcbea87 100644 --- a/orx-shade-styles/build.gradle.kts +++ b/orx-shade-styles/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { implementation(project(":orx-noise")) implementation(project(":orx-shapes")) implementation(project(":orx-image-fit")) + implementation(project(":orx-camera")) } } } diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/patterns/PatternBase.kt b/orx-shade-styles/src/commonMain/kotlin/fills/patterns/PatternBase.kt new file mode 100644 index 00000000..b34fc20d --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/patterns/PatternBase.kt @@ -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() + } +} \ No newline at end of file diff --git a/orx-shade-styles/src/commonMain/kotlin/fills/patterns/PatternBuilder.kt b/orx-shade-styles/src/commonMain/kotlin/fills/patterns/PatternBuilder.kt new file mode 100644 index 00000000..bd5a370a --- /dev/null +++ b/orx-shade-styles/src/commonMain/kotlin/fills/patterns/PatternBuilder.kt @@ -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 = ObservableHashmap(mutableMapOf()) {} + override var parameterValues: MutableMap = 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() +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns01.kt b/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns01.kt new file mode 100644 index 00000000..b86a4695 --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns01.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns02.kt b/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns02.kt new file mode 100644 index 00000000..c40d502e --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns02.kt @@ -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)) + } + } +} \ No newline at end of file diff --git a/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns03.kt b/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns03.kt new file mode 100644 index 00000000..3d5683da --- /dev/null +++ b/orx-shade-styles/src/jvmDemo/kotlin/patterns/DemoPatterns03.kt @@ -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 { + 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) + + + + } + } +} \ No newline at end of file