Skip to content

Commit

Permalink
add more color extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
jordond committed Feb 2, 2024
1 parent fc9c52e commit 8930e11
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import com.materialkolor.PaletteStyle
import com.materialkolor.demo.theme.AppTheme
import com.materialkolor.ktx.darken
import com.materialkolor.ktx.fromColor
import com.materialkolor.ktx.harmonize
import com.materialkolor.ktx.lighten
import com.materialkolor.palettes.TonalPalette
import kotlin.math.round

val SampleColors = listOf(
Color(0xFFD32F2F),
Expand Down Expand Up @@ -313,6 +317,63 @@ internal fun App() {
}
}
}

Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
var color by remember(seedColor) { mutableStateOf(seedColor) }

Button(onClick = { color = color.lighten(1.1f) }) {
Text("Lighten")
}
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(height = 32.dp, width = 80.dp)
.clip(MaterialTheme.shapes.small)
.background(color)
)
Spacer(modifier = Modifier.height(4.dp))
Text(text = color.toArgb().toString(16))
Spacer(modifier = Modifier.height(4.dp))
Button(onClick = { color = color.darken(1.1f) }) {
Text("Darken")
}
}

Column(
modifier = Modifier.fillMaxWidth(),
) {
repeat(10) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth(),
) {
val ratio = 1.1f + it / 10f
val darker by remember(seedColor) { mutableStateOf(seedColor.darken(ratio)) }
val lighter by remember(seedColor) { mutableStateOf(seedColor.lighten(ratio)) }

Text(text = "Darken ${ratio.roundToTwoDecimalPlaces()}")
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(height = 32.dp, width = 80.dp)
.clip(MaterialTheme.shapes.small)
.background(darker)
)

Text(text = "Lighten ${ratio.roundToTwoDecimalPlaces()}")
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(height = 32.dp, width = 80.dp)
.clip(MaterialTheme.shapes.small)
.background(lighter)
)
}
}
}
}
}
}
Expand All @@ -335,3 +396,7 @@ fun ColorBox(
)
}
}

private fun Float.roundToTwoDecimalPlaces(): Float {
return round(this * 100) / 100
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ public object Contrast {
* also known as relative luminance.
*
* The equation is ratio = lighter Y + 5 / darker Y + 5.
*
* @param[y1] Y in XYZ, relative luminance.
* @param[y2] Y in XYZ, relative luminance.
* @return Contrast ratio of two colors.
*/
public fun ratioOfYs(y1: Double, y2: Double): Double {
val lighter: Double = max(y1, y2)
Expand All @@ -99,7 +103,7 @@ public object Contrast {
}

/**
* Contrast ratio of two tones. T in HCT, L* in L*a*b*. Also known as luminance or perpectual
* Contrast ratio of two tones. T in HCT, L* in L*a*b*. Also known as luminance or perpetual
* luminance.
*
* Contrast ratio is defined using Y in XYZ, relative luminance. However, relative luminance is
Expand All @@ -112,6 +116,10 @@ public object Contrast {
* of a ratio, a linear difference. This allows a designer to determine what they need to adjust a
* color's lightness to in order to reach their desired contrast, instead of guessing & checking
* with hex codes.
*
* @param[tone1] T in HCT, L* in L*a*b*.
* @param[tone2] T in HCT, L* in L*a*b*.
* @return Contrast ratio of two tones. T in HCT, L* in L*a*b*.
*/
public fun ratioOfTones(tone1: Double, tone2: Double): Double {
return ratioOfYs(yFromLstar(tone1), yFromLstar(tone2))
Expand All @@ -121,8 +129,9 @@ public object Contrast {
* Returns T in HCT, L* in L*a*b* >= tone parameter that ensures ratio with input T/L*.
* Returns -1 if ratio cannot be achieved.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
* @return T in HCT, L* in L*a*b* >= tone parameter that ensures ratio with input T/L*.
*/
public fun lighter(tone: Double, ratio: Double): Double {
if (tone < 0.0 || tone > 100.0) {
Expand All @@ -146,26 +155,52 @@ public object Contrast {
} else returnValue
}

/**
* Returns T in HCT, L* in L*a*b* >= tone parameter that ensures ratio with input T/L*.
* Returns -1 if ratio cannot be achieved.
*
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
* @return T in HCT, L* in L*a*b* >= tone parameter that ensures ratio with input T/L*.
*/
public fun lighter(tone: Double, ratio: Float): Float {
return lighter(tone, ratio.toDouble()).toFloat()
}

/**
* Tone >= tone parameter that ensures ratio. 100 if ratio cannot be achieved.
*
* This method is unsafe because the returned value is guaranteed to be in bounds, but, the in
* bounds return value may not reach the desired ratio.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
*/
public fun lighterUnsafe(tone: Double, ratio: Double): Double {
val lighterSafe = lighter(tone, ratio)
return if (lighterSafe < 0.0) 100.0 else lighterSafe
}

/**
* Tone >= tone parameter that ensures ratio. 100 if ratio cannot be achieved.
*
* This method is unsafe because the returned value is guaranteed to be in bounds, but, the in
* bounds return value may not reach the desired ratio.
*
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
*/
public fun lighterUnsafe(tone: Double, ratio: Float): Float {
return lighterUnsafe(tone, ratio.toDouble()).toFloat()
}

/**
* Returns T in HCT, L* in L*a*b* <= tone parameter that ensures ratio with input T/L*. Returns -1
* if ratio cannot be achieved.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
* @return T in HCT, L* in L*a*b* <= tone parameter that ensures ratio with input T/L*.
*/
public fun darker(tone: Double, ratio: Double): Double {
if (tone < 0.0 || tone > 100.0) {
Expand All @@ -191,17 +226,42 @@ public object Contrast {
} else returnValue
}

/**
* Returns T in HCT, L* in L*a*b* <= tone parameter that ensures ratio with input T/L*. Returns -1
* if ratio cannot be achieved.
*
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
* @return T in HCT, L* in L*a*b* <= tone parameter that ensures ratio with input T/L*.
*/
public fun darker(tone: Double, ratio: Float): Float {
return darker(tone, ratio.toDouble()).toFloat()
}

/**
* Tone <= tone parameter that ensures ratio. 0 if ratio cannot be achieved.
*
* This method is unsafe because the returned value is guaranteed to be in bounds, but, the in
* bounds return value may not reach the desired ratio.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
*/
public fun darkerUnsafe(tone: Double, ratio: Double): Double {
val darkerSafe = darker(tone, ratio)
return max(0.0, darkerSafe)
}

/**
* Tone <= tone parameter that ensures ratio. 0 if ratio cannot be achieved.
*
* This method is unsafe because the returned value is guaranteed to be in bounds, but, the in
* bounds return value may not reach the desired ratio.
*
* @param[tone] Tone return value must contrast with.
* @param[ratio] Desired contrast ratio of return value and tone parameter.
*/
public fun darkerUnsafe(tone: Double, ratio: Float): Float {
return darkerUnsafe(tone, ratio.toDouble()).toFloat()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public object DislikeAnalyzer {
* Returns true if color is disliked.
*
* Disliked is defined as a dark yellow-green that is not neutral.
*
* @param[hct] Hct color to check
* @return true if color is disliked
*/
public fun isDisliked(hct: Hct): Boolean {
val huePasses = round(hct.hue) in 90.0..111.0
Expand All @@ -44,6 +47,9 @@ public object DislikeAnalyzer {

/**
* If color is disliked, lighten it to make it likable.
*
* @param[hct] Hct color to check
* @return Lightened Hct color that is not disliked
*/
public fun fixIfDisliked(hct: Hct): Hct {
return if (isDisliked(hct)) Hct.from(hct.hue, hct.chroma, 70.0) else hct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ import kotlin.math.min
/**
* An intermediate concept between the key color for a UI theme, and a full color scheme. 5 sets of
* tones are generated, all except one use the same hue as the key color, and all vary in chroma.
*
* @constructor Create a new CorePalette
* @param[argb] ARGB representation of a color
* @param[isContent] Whether the color is used as a content color
*/
public class CorePalette private constructor(argb: Int, isContent: Boolean) {

public var a1: TonalPalette
public var a2: TonalPalette
public var a3: TonalPalette
public var n1: TonalPalette
public var n2: TonalPalette
public var error: TonalPalette
public val a1: TonalPalette
public val a2: TonalPalette
public val a3: TonalPalette
public val n1: TonalPalette
public val n2: TonalPalette
public val error: TonalPalette

init {
val hct = Hct.fromInt(argb)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.materialkolor.ktx
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import com.github.ajalt.colormath.model.HSL
import com.materialkolor.contrast.Contrast
import com.materialkolor.dislike.DislikeAnalyzer
import com.materialkolor.internal.toColormathColor
import com.materialkolor.internal.toComposeColor
import com.materialkolor.utils.ColorUtils
Expand All @@ -17,6 +19,54 @@ public fun Color.isLight(): Boolean {
return this != Color.Transparent && ColorUtils.calculateLuminance(toArgb()) > 0.5;
}

/**
* Lighten the color by the given ratio.
*
* @receiver[Color] to lighten.
* @param[ratio] the ratio to lighten the color by.
* @return [Color] The lightened color or the original color if the lightened color is not valid.
*/
public fun Color.lighten(ratio: Float = 1.0f): Color {
val hct = toHct()
val tone = Contrast.lighter(hct.tone, ratio.toDouble()).takeIf { it > -1 }
return if (tone == null) this else hct.withTone(tone).toColor()
}

/**
* Darken the color by the given ratio.
*
* @receiver[Color] to darken.
* @param[ratio] the ratio to darken the color by.
* @return [Color] The darkened color or the original color if the darkened color is not valid.
*/
public fun Color.darken(ratio: Float = 1.1f): Color {
val hct = toHct()
val tone = Contrast.darker(hct.tone, ratio.toDouble()).takeIf { it > -1 }
return if (tone == null) this else hct.withTone(tone).toColor()
}

/**
* Returns true if color is disliked.
*
* Disliked is defined as a dark yellow-green that is not neutral.
*
* @receiver The [Color] to check.
* @return true if color is disliked
*/
public fun Color.isDisliked(): Boolean {
return DislikeAnalyzer.isDisliked(toHct())
}

/**
* If color is disliked, lighten it to make it likable.
*
* @receiver The [Color] to check.
* @return Lightened [Color] that is not disliked
*/
public fun Color.fixIfDisliked(): Color {
return DislikeAnalyzer.fixIfDisliked(toHct()).toColor()
}

/**
* Create a [Color] with the same hue as this color, but with the saturation and lightness of [other].
*
Expand Down
20 changes: 20 additions & 0 deletions material-kolor/src/commonMain/kotlin/com/materialkolor/ktx/Hct.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ public fun Hct.toColor(): Color {
return Color(toInt())
}

/**
* Convert a list of HCT to a list of Compose [Color].
*
* @receiver The list of HCT to convert.
* @return The list of Compose [Color] representation of the HCT.
*/
public fun List<Hct>.toColors(): List<Color> {
return map { it.toColor() }
}

/**
* Convert a Compose [Color] to [Hct].
*
Expand All @@ -32,4 +42,14 @@ public fun Hct.toColor(): Color {
*/
public fun Color.toHct(): Hct {
return Hct.from(this)
}

/**
* Convert a list of Compose [Color] to a list of HCT.
*
* @receiver The list of colors to convert.
* @return The list of HCT representation of the colors.
*/
public fun List<Color>.toHcts(): List<Hct> {
return map { it.toHct() }
}

0 comments on commit 8930e11

Please sign in to comment.