[orx-color] Add FettePalette Kotlin port and demos
This commit is contained in:
257
orx-color/src/commonMain/kotlin/fettepalette/FettePalette.kt
Normal file
257
orx-color/src/commonMain/kotlin/fettepalette/FettePalette.kt
Normal file
@@ -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<ColorRGBa>, val darkColors: List<ColorRGBa>, val lightColors: List<ColorRGBa>)
|
||||
|
||||
@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<ColorRGBa>()
|
||||
val lightColors = mutableListOf<ColorRGBa>()
|
||||
val darkColors = mutableListOf<ColorRGBa>()
|
||||
|
||||
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
|
||||
)
|
||||
86
orx-color/src/demo/kotlin/DemoFettePalette01.kt
Normal file
86
orx-color/src/demo/kotlin/DemoFettePalette01.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
orx-color/src/demo/kotlin/DemoFettePalette02.kt
Normal file
77
orx-color/src/demo/kotlin/DemoFettePalette02.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user