diff --git a/orx-color/src/commonMain/kotlin/fettepalette/FettePalette.kt b/orx-color/src/commonMain/kotlin/fettepalette/FettePalette.kt new file mode 100644 index 00000000..7dbc883b --- /dev/null +++ b/orx-color/src/commonMain/kotlin/fettepalette/FettePalette.kt @@ -0,0 +1,257 @@ +package org.openrndr.extra.color.fettepalette + +import org.openrndr.color.ColorHSLa +import org.openrndr.color.ColorHSVa +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.color.spaces.ColorOKHSLa +import org.openrndr.extra.color.spaces.ColorOKHSVa +import org.openrndr.extra.color.spaces.toOKHSLa +import org.openrndr.extra.parameters.* +import org.openrndr.math.Vector2 +import org.openrndr.math.clamp +import org.openrndr.math.mod_ +import kotlin.math.* + +/* +Converted to Kotlin from https://github.com/meodai/fettepalette/blob/main/src/index.ts + +MIT License + +Copyright (c) 2021 David Aerne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +interface Curve { + fun pointOnCurve( + i: Double, + total: Double, + curveAccent: Double, + min: Vector2 = Vector2.ZERO, + max: Vector2 = Vector2.ZERO + ): Vector2 + + fun remap(v: Vector2, min: Vector2, max: Vector2): Vector2 { + var x = v.x + var y = v.y + + x = min.x + x.coerceIn(0.0, 1.0) * (max.x - min.x) + y = min.y + y.coerceIn(0.0, 1.0) * (max.y - min.y) + + return Vector2(x, y) + } +} + +object Lamé : Curve { + override fun pointOnCurve(i: Double, total: Double, curveAccent: Double, min: Vector2, max: Vector2): Vector2 { + val limit = PI / 2 + val percentile = i / total + val t = percentile * limit + val exp = 2 / (2 + 20 * curveAccent) + val cosT = cos(t) + val sinT = sin(t) + val x = sign(cosT) * abs(cosT).pow(exp) + val y = sign(sinT) * abs(sinT).pow(exp) + return remap(Vector2(x, y), min, max) + } +} + +object Arc : Curve { + override fun pointOnCurve(i: Double, total: Double, curveAccent: Double, min: Vector2, max: Vector2): Vector2 { + val limit = PI / 2 + val percentile = i / total + val t = percentile * limit + val slice = limit / total + val y = cos(-PI / 2 + i * slice + curveAccent) + val x = sin(PI / 2 + i * slice - curveAccent) + return remap(Vector2(x, y), min, max) + } +} + +class ColorRamp(val baseColors: List, val darkColors: List, val lightColors: List) + +@Description("Color ramp parameters") +class ColorRampParameters { + @IntParameter("total", 3, 16, 0) + var total = 3 + + @DoubleParameter("center hue", -180.0, 180.0, 3, 1) + var centerHue = 0.0 + + @DoubleParameter("hue cycle", 0.0, 1.0, 3, 2) + var hueCycle = 0.3 + + @DoubleParameter("offset tint", 0.0, 1.0, 3, 3) + var offsetTint = 0.1 + + @DoubleParameter("offset shade", 0.0, 1.0, 3, 4) + var offsetShade = 0.1 + + @DoubleParameter("curve accent", 0.0, 1.0, 3, 5) + var curveAccent = 0.0 + + @DoubleParameter("tint shade hue shift", 0.0, 1.0, 3, 6) + var tintShadeHueShift = 0.1 + + @DoubleParameter("offset mod tint", 0.0, 1.0, 3, 7) + var offsetCurveModTint = 0.03 + + @DoubleParameter("offset mod shade", 0.0, 1.0, 3, 8) + var offsetCurveModShade = 0.03 + + @Vector2Parameter("min saturation/light", min = 0.0, max = 1.0, precision = 3, order = 9) + var minSaturationLight = Vector2.ZERO + + @Vector2Parameter("max saturation/light", min = 0.0, max = 1.0, precision = 3, order = 10) + var maxSaturationLight = Vector2.ONE + + @BooleanParameter("use OKHSV", order = 11) + var useOK = false +} + + +/** + * @param total total of base colors in the ramp + * @param centerHue at what hue should the generation start at + * @param hueCycle hsl spins how much should the hue change over the curve, 0: not at all, 1: one full rainbow + * @param offsetTint offset for the tints + * @param offsetShade offset of the shades + * @param curveAccent how accentuated is the curve (depends heavily on curveMethod) + * @param tintShadeHueShift defines how shifted the hue is for the shades and the tints + * @param offsetCurveModTint modifies the tint curve + * @param offsetCurveModShade modifies the shade curve + * @param minSaturationLight defines the min saturation and light of all the colors + * @param maxSaturationLight defines the max saturation and light of all the colors + * @param useOK use OKHSV and OKHSL spaces + */ +fun generateColorRamp( + total: Int = 3, + centerHue: Double = 0.0, + hueCycle: Double = 0.3, + offsetTint: Double = 0.1, + offsetShade: Double = 0.1, + curveAccent: Double = 0.0, + tintShadeHueShift: Double = 0.1, + curveMethod: Curve = Lamé, + offsetCurveModTint: Double = 0.03, + offsetCurveModShade: Double = 0.03, + minSaturationLight: Vector2 = Vector2.ZERO, + maxSaturationLight: Vector2 = Vector2.ONE, + useOK: Boolean = false, +): ColorRamp { + val baseColors = mutableListOf() + val lightColors = mutableListOf() + val darkColors = mutableListOf() + + val okHueAdjust = if (useOK) 30.0 else 0.0 + + for (i in 1 until total + 1) { + val (x, y) = curveMethod.pointOnCurve( + i.toDouble(), + total + 1.0, + curveAccent, + minSaturationLight, + maxSaturationLight + ) + val h = (okHueAdjust + 360.0 + + (-180.0 * hueCycle + (centerHue + i * (360 / (total + 1)) * hueCycle)) + ) % 360 + + val hsv = if (useOK) { + ColorOKHSVa(h, x, y) + } else ColorHSVa(h, x, y) + val hsl = if (useOK) { + hsv.toRGBa().toOKHSLa() + } else hsv.toRGBa().toHSLa() + baseColors.add(hsl.toRGBa().toSRGB()) + + val (xl, yl) = curveMethod.pointOnCurve( + i.toDouble(), total + 1.0, curveAccent + offsetCurveModTint, + minSaturationLight, + maxSaturationLight + ) + + val hslLight = if (useOK) ColorOKHSVa(h, xl, yl).toRGBa().toOKHSLa() else ColorHSVa(h, xl, yl).toRGBa().toHSLa() + + if (useOK) { + hslLight as ColorOKHSLa + lightColors.add( + ColorOKHSLa( + (hslLight.h + 360.0 * tintShadeHueShift).mod_(360.0), + (hslLight.s - offsetTint).clamp(0.0, 1.0), + (hslLight.l + offsetTint).clamp(0.0, 1.0) + ).toRGBa().toSRGB() + ) + } else { + hslLight as ColorHSLa + lightColors.add( + ColorHSLa( + (hslLight.h + 360.0 * tintShadeHueShift).mod_(360.0), + (hslLight.s - offsetTint).clamp(0.0, 1.0), + (hslLight.l + offsetTint).clamp(0.0, 1.0) + ).toRGBa().toSRGB() + ) + } + + val (xd, yd) = curveMethod.pointOnCurve( + i.toDouble(), total + 1.0, curveAccent - offsetCurveModShade, + minSaturationLight, + maxSaturationLight + ) + + val hslDark = if (useOK) ColorOKHSVa(h, xd, yd).toRGBa().toOKHSLa() else ColorHSVa(h, xd, yd).toRGBa().toHSLa() + + if (useOK) { + hslDark as ColorOKHSLa + darkColors.add( + ColorOKHSLa( + (hslDark.h - 360.0 * tintShadeHueShift).mod_(360.0), + (hslDark.s - offsetShade).clamp(0.0, 1.0), + (hslDark.l - offsetShade).clamp(0.0, 1.0) + ).toRGBa().toSRGB() + ) + } else { + hslDark as ColorHSLa + darkColors.add( + ColorHSLa( + (hslDark.h - 360.0 * tintShadeHueShift).mod_(360.0), + (hslDark.s - offsetShade).clamp(0.0, 1.0), + (hslDark.l - offsetShade).clamp(0.0, 1.0) + ).toRGBa().toSRGB() + ) + } + } + return ColorRamp(baseColors, darkColors, lightColors) +} + +fun generateColorRamp(parameters: ColorRampParameters) + = generateColorRamp(total = parameters.total, + centerHue = parameters.centerHue, + hueCycle = parameters.hueCycle, + offsetTint = parameters.offsetTint, + offsetShade = parameters.offsetShade, + curveAccent = parameters.curveAccent, + tintShadeHueShift = parameters.tintShadeHueShift, + curveMethod = Lamé, + offsetCurveModTint = parameters.offsetCurveModTint, + offsetCurveModShade = parameters.offsetCurveModShade, + minSaturationLight = parameters.minSaturationLight, + maxSaturationLight = parameters.maxSaturationLight, + useOK = parameters.useOK +) \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoFettePalette01.kt b/orx-color/src/demo/kotlin/DemoFettePalette01.kt new file mode 100644 index 00000000..7c08f44a --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoFettePalette01.kt @@ -0,0 +1,86 @@ +import org.openrndr.application +import org.openrndr.draw.isolated +import org.openrndr.extra.color.fettepalette.ColorRamp +import org.openrndr.extra.color.fettepalette.Lamé +import org.openrndr.extra.color.fettepalette.generateColorRamp +import org.openrndr.math.Vector2 +import kotlin.random.Random + +fun main() { + application { + configure { + width = 720 + height = 720 + } + + program { + val total = 9 + + extend { + val ramp = generateColorRamp( + total = total, + centerHue = (mouse.position.x / width) * 360.0, + curveMethod = Lamé, + hueCycle = mouse.position.y / height, + curveAccent = 0.0, + offsetTint = 0.01, + offsetShade = 0.01, + tintShadeHueShift = 0.01, + offsetCurveModTint = 0.03, + offsetCurveModShade = 0.03, + minSaturationLight = Vector2.ZERO, + maxSaturationLight = Vector2.ONE, + useOK = true + ) + + fun rampSquare(ramp: ColorRamp, random: Random, position: Vector2, width: Double) { + drawer.isolated { + drawer.fill = ramp.baseColors.random(random).toRGBa() + drawer.stroke = null + drawer.rectangle(position, width, width) + + drawer.fill = ramp.lightColors.random(random).toRGBa() + drawer.rectangle(position + Vector2(width / 4.0, width / 4.0), width / 4.0, width / 2.0) + + val dc = ramp.darkColors.shuffled(random).take(2) + + drawer.fill = dc[0].toRGBa() + drawer.rectangle(position + Vector2(width / 2.0, width / 4.0), width / 4.0, width / 4.0) + + drawer.fill = dc[1].toRGBa() + drawer.rectangle(position + Vector2(width / 2.0, width / 2.0), width / 4.0, width / 4.0) + } + } + + + drawer.isolated { + for ((index, i) in ramp.lightColors.withIndex()) { + drawer.stroke = null + drawer.fill = i.toRGBa() + drawer.rectangle(20.0, 20.0, 50.0, 50.0) + drawer.translate(50.0, 0.0) + } + } + drawer.isolated { + for ((index, i) in ramp.baseColors.withIndex()) { + drawer.stroke = null + drawer.fill = i.toRGBa() + drawer.rectangle(20.0, 70.0, 50.0, 50.0) + drawer.translate(50.0, 0.0) + } + } + drawer.isolated { + for ((index, i) in ramp.darkColors.withIndex()) { + drawer.stroke = null + drawer.fill = i.toRGBa() + drawer.rectangle(20.0, 120.0, 50.0, 50.0) + drawer.translate(50.0, 0.0) + } + } + + val random = Random(seconds.toInt()) + rampSquare(ramp, random, Vector2(180.0, 180.0), 360.0) + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoFettePalette02.kt b/orx-color/src/demo/kotlin/DemoFettePalette02.kt new file mode 100644 index 00000000..e5d15891 --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoFettePalette02.kt @@ -0,0 +1,77 @@ +import org.openrndr.application +import org.openrndr.draw.isolated +import org.openrndr.extra.color.fettepalette.ColorRamp +import org.openrndr.extra.color.fettepalette.ColorRampParameters +import org.openrndr.extra.color.fettepalette.generateColorRamp +import org.openrndr.extra.gui.GUI +import org.openrndr.extra.gui.addTo +import org.openrndr.math.Vector2 +import kotlin.random.Random + +fun main() { + application { + configure { + width = 720 + height = 720 + } + + program { + val gui = GUI() + val parameters = ColorRampParameters() + parameters.addTo(gui) + + extend(gui) + extend { + val ramp = generateColorRamp(parameters) + fun rampSquare(ramp: ColorRamp, random: Random, position: Vector2, width: Double) { + drawer.isolated { + drawer.fill = ramp.baseColors.random(random).toRGBa() + drawer.stroke = null + drawer.rectangle(position, width, width) + + drawer.fill = ramp.lightColors.random(random).toRGBa() + drawer.rectangle(position + Vector2(width / 4.0, width / 4.0), width / 4.0, width / 2.0) + + val dc = ramp.darkColors.shuffled(random).take(2) + + drawer.fill = dc[0].toRGBa() + drawer.rectangle(position + Vector2(width / 2.0, width / 4.0), width / 4.0, width / 4.0) + + drawer.fill = dc[1].toRGBa() + drawer.rectangle(position + Vector2(width / 2.0, width / 2.0), width / 4.0, width / 4.0) + } + } + + drawer.translate(200.0, 0.0) + + drawer.isolated { + for ((index, i) in ramp.lightColors.withIndex()) { + drawer.stroke = null + drawer.fill = i.toRGBa() + drawer.rectangle(20.0, 20.0, 50.0, 50.0) + drawer.translate(50.0, 0.0) + } + } + drawer.isolated { + for ((index, i) in ramp.baseColors.withIndex()) { + drawer.stroke = null + drawer.fill = i.toRGBa() + drawer.rectangle(20.0, 70.0, 50.0, 50.0) + drawer.translate(50.0, 0.0) + } + } + drawer.isolated { + for ((index, i) in ramp.darkColors.withIndex()) { + drawer.stroke = null + drawer.fill = i.toRGBa() + drawer.rectangle(20.0, 120.0, 50.0, 50.0) + drawer.translate(50.0, 0.0) + } + } + + val random = Random(seconds.toInt()) + rampSquare(ramp, random, Vector2(20.0, 180.0), 360.0) + } + } + } +} \ No newline at end of file