Update default colors
This commit is contained in:
@@ -29,7 +29,7 @@ fun GuardedPreference(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.extraSmall)
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||||
) {
|
) {
|
||||||
if (locked) {
|
if (locked) {
|
||||||
Banner(
|
Banner(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ fun <T> ListPreference(
|
|||||||
items: List<ListPreferenceItem<T>>,
|
items: List<ListPreferenceItem<T>>,
|
||||||
value: T,
|
value: T,
|
||||||
summary: String? = items.firstOrNull { value == it.value }?.label,
|
summary: String? = items.firstOrNull { value == it.value }?.label,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||||
onValueChanged: (T) -> Unit,
|
onValueChanged: (T) -> Unit,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
itemLabel: @Composable RowScope.(item: ListPreferenceItem<T>) -> Unit = {
|
itemLabel: @Composable RowScope.(item: ListPreferenceItem<T>) -> Unit = {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ fun Preference(
|
|||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
controls: @Composable (() -> Unit)? = null,
|
controls: @Composable (() -> Unit)? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -92,7 +92,7 @@ fun Preference(
|
|||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
controls: @Composable (() -> Unit)? = null,
|
controls: @Composable (() -> Unit)? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||||
) {
|
) {
|
||||||
Preference(
|
Preference(
|
||||||
title = {
|
title = {
|
||||||
@@ -121,7 +121,7 @@ fun Preference(
|
|||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
controls: @Composable (() -> Unit)? = null,
|
controls: @Composable (() -> Unit)? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||||
) {
|
) {
|
||||||
Preference(
|
Preference(
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -28,13 +28,9 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -157,8 +153,8 @@ fun PreferenceScreen(
|
|||||||
content = content,
|
content = content,
|
||||||
verticalArrangement = verticalArrangement,
|
verticalArrangement = verticalArrangement,
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
top = it.calculateTopPadding(),
|
top = it.calculateTopPadding() + 12.dp,
|
||||||
bottom = it.calculateBottomPadding() + 4.dp,
|
bottom = it.calculateBottomPadding() + 12.dp,
|
||||||
start = 12.dp,
|
start = 12.dp,
|
||||||
end = 12.dp
|
end = 12.dp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ fun PreferenceWithSwitch(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.extraSmall)
|
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ fun SliderPreference(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.extraSmall)
|
.background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall)
|
||||||
.padding(
|
.padding(
|
||||||
start = if (icon != null || iconPadding) 8.dp else 16.dp,
|
start = if (icon != null || iconPadding) 8.dp else 16.dp,
|
||||||
end = 16.dp,
|
end = 16.dp,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ fun SwitchPreference(
|
|||||||
value: Boolean,
|
value: Boolean,
|
||||||
onValueChanged: (Boolean) -> Unit,
|
onValueChanged: (Boolean) -> Unit,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||||
) {
|
) {
|
||||||
Preference(
|
Preference(
|
||||||
title = title,
|
title = title,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ fun Color.Companion.hct(hue: Float, chroma: Float, tone: Float): Color {
|
|||||||
fun Color.atTone(tone: Int): Color {
|
fun Color.atTone(tone: Int): Color {
|
||||||
return Color(
|
return Color(
|
||||||
Hct.fromInt(this.toArgb()).apply {
|
Hct.fromInt(this.toArgb()).apply {
|
||||||
this.tone = tone.toDouble()
|
this.setTone(tone.toDouble())
|
||||||
}.toInt()
|
}.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ fun ColorSchemePreferenceCategory(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.surfaceContainerLowest,
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
MaterialTheme.shapes.extraSmall
|
MaterialTheme.shapes.extraSmall
|
||||||
)
|
)
|
||||||
.horizontalScroll(rememberScrollState())
|
.horizontalScroll(rememberScrollState())
|
||||||
|
|||||||
@@ -65,15 +65,15 @@ import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
|||||||
import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf
|
import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf
|
||||||
import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
|
import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
|
||||||
import de.mm20.launcher2.ui.theme.colorscheme.systemCorePalette
|
import de.mm20.launcher2.ui.theme.colorscheme.systemCorePalette
|
||||||
|
import hct.Hct
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import palettes.CorePalette
|
import scheme.SchemeExpressive
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ColorSchemeSettingsRoute(
|
data class ColorSchemeSettingsRoute(
|
||||||
@Serializable(with = UUIDSerializer::class) val id: UUID
|
@Serializable(with = UUIDSerializer::class) val id: UUID
|
||||||
): NavKey
|
) : NavKey
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ColorSchemeSettingsScreen(themeId: UUID) {
|
fun ColorSchemeSettingsScreen(themeId: UUID) {
|
||||||
@@ -91,7 +91,7 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
val previewColorScheme =
|
val previewColorScheme =
|
||||||
theme?.let { if (previewDarkTheme) darkColorSchemeOf(it) else lightColorSchemeOf(it) }
|
theme?.let { if (previewDarkTheme) darkColorSchemeOf(it) else lightColorSchemeOf(it) }
|
||||||
|
|
||||||
val systemPalette = systemCorePalette()
|
val systemPalette = systemCorePalette(dark)
|
||||||
|
|
||||||
val mergedCorePalette by remember(theme?.corePalette, systemPalette) {
|
val mergedCorePalette by remember(theme?.corePalette, systemPalette) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@@ -175,7 +175,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
defaultValue = systemPalette.secondary,
|
defaultValue = systemPalette.secondary,
|
||||||
autoGenerate = {
|
autoGenerate = {
|
||||||
theme!!.corePalette.primary?.let {
|
theme!!.corePalette.primary?.let {
|
||||||
CorePalette.of(it).a2.keyColor.toInt()
|
SchemeExpressive(
|
||||||
|
Hct.fromInt(it), false, 0.0,
|
||||||
|
).secondaryPaletteKeyColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -194,7 +196,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
defaultValue = systemPalette.tertiary,
|
defaultValue = systemPalette.tertiary,
|
||||||
autoGenerate = {
|
autoGenerate = {
|
||||||
theme!!.corePalette.primary?.let {
|
theme!!.corePalette.primary?.let {
|
||||||
CorePalette.of(it).a3.keyColor.toInt()
|
SchemeExpressive(
|
||||||
|
Hct.fromInt(it), false, 0.0,
|
||||||
|
).tertiaryPaletteKeyColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -213,7 +217,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
defaultValue = systemPalette.neutral,
|
defaultValue = systemPalette.neutral,
|
||||||
autoGenerate = {
|
autoGenerate = {
|
||||||
theme!!.corePalette.primary?.let {
|
theme!!.corePalette.primary?.let {
|
||||||
CorePalette.of(it).n1.keyColor.toInt()
|
SchemeExpressive(
|
||||||
|
Hct.fromInt(it), false, 0.0,
|
||||||
|
).neutralPaletteKeyColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -232,7 +238,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
defaultValue = systemPalette.neutralVariant,
|
defaultValue = systemPalette.neutralVariant,
|
||||||
autoGenerate = {
|
autoGenerate = {
|
||||||
theme!!.corePalette.primary?.let {
|
theme!!.corePalette.primary?.let {
|
||||||
CorePalette.of(it).n2.keyColor.toInt()
|
SchemeExpressive(
|
||||||
|
Hct.fromInt(it), false, 0.0,
|
||||||
|
).neutralVariantPaletteKeyColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -251,7 +259,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
defaultValue = systemPalette.error,
|
defaultValue = systemPalette.error,
|
||||||
autoGenerate = {
|
autoGenerate = {
|
||||||
theme!!.corePalette.primary?.let {
|
theme!!.corePalette.primary?.let {
|
||||||
CorePalette.of(it).error.keyColor.toInt()
|
SchemeExpressive(
|
||||||
|
Hct.fromInt(it), false, 0.0,
|
||||||
|
).errorPalette.keyColor.toInt()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -790,7 +800,11 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(painterResource(R.drawable.search_24px), null, tint = MaterialTheme.colorScheme.onSurface)
|
Icon(
|
||||||
|
painterResource(R.drawable.search_24px),
|
||||||
|
null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1080,7 +1094,7 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { },
|
onClick = { },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 16.dp)
|
.padding(end = 16.dp)
|
||||||
.align(Alignment.CenterVertically),
|
.align(Alignment.CenterVertically),
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ fun ColorSwatch(
|
|||||||
else 40f
|
else 40f
|
||||||
}
|
}
|
||||||
it.apply {
|
it.apply {
|
||||||
this.tone = tone.toDouble()
|
this.setTone(tone.toDouble())
|
||||||
}.toInt()
|
}.toInt()
|
||||||
})
|
})
|
||||||
val borderColor = Color(Hct.fromInt(color.toArgb()).let {
|
val borderColor = Color(Hct.fromInt(color.toArgb()).let {
|
||||||
val tone = if (darkTheme) 30f else 80f
|
val tone = if (darkTheme) 30f else 80f
|
||||||
it.apply {
|
it.apply {
|
||||||
this.tone = tone.toDouble()
|
this.setTone(tone.toDouble())
|
||||||
}.toInt()
|
}.toInt()
|
||||||
})
|
})
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ fun CorePaletteColorPreference(
|
|||||||
.clickable(
|
.clickable(
|
||||||
onClick = { showDialog = true },
|
onClick = { showDialog = true },
|
||||||
)
|
)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ fun ThemeColorPreference(
|
|||||||
.clickable(
|
.clickable(
|
||||||
onClick = { showDialog = true },
|
onClick = { showDialog = true },
|
||||||
)
|
)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ fun CrashReportScreen(fileName: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||||
.horizontalScroll(
|
.horizontalScroll(
|
||||||
rememberScrollState()
|
rememberScrollState()
|
||||||
),
|
),
|
||||||
@@ -80,7 +80,7 @@ fun CrashReportScreen(fileName: String) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.medium)
|
.background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.medium)
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
) {
|
) {
|
||||||
Text(text = "Device Information", style = MaterialTheme.typography.titleMedium)
|
Text(text = "Device Information", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ fun IconsSettingsScreen() {
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(
|
||||||
MaterialTheme.colorScheme.surfaceContainerLowest,
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
MaterialTheme.shapes.extraSmall
|
MaterialTheme.shapes.extraSmall
|
||||||
)
|
)
|
||||||
.padding(vertical = 24.dp, horizontal = 8.dp)
|
.padding(vertical = 24.dp, horizontal = 8.dp)
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ fun ShapePreference(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.extraSmall)
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = { showDialog = true },
|
onClick = { showDialog = true },
|
||||||
)
|
)
|
||||||
@@ -733,7 +733,7 @@ private fun ShapePreview(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.extraSmall)
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
.horizontalScroll(rememberScrollState())
|
.horizontalScroll(rememberScrollState())
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ private fun TransparencyPreference(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.extraSmall)
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -777,7 +777,7 @@ private fun FontPreference(
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.background(
|
modifier = Modifier.background(
|
||||||
MaterialTheme.colorScheme.surface,
|
MaterialTheme.colorScheme.surfaceBright,
|
||||||
MaterialTheme.shapes.extraSmall
|
MaterialTheme.shapes.extraSmall
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -1309,7 +1309,7 @@ private fun TypographyPreview(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.extraSmall)
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
.horizontalScroll(rememberScrollState())
|
.horizontalScroll(rememberScrollState())
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -20,21 +20,27 @@ import de.mm20.launcher2.themes.colors.Colors as ThemeColors
|
|||||||
import de.mm20.launcher2.themes.colors.get
|
import de.mm20.launcher2.themes.colors.get
|
||||||
import de.mm20.launcher2.themes.colors.merge
|
import de.mm20.launcher2.themes.colors.merge
|
||||||
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
||||||
|
import dynamiccolor.ColorSpec
|
||||||
|
import hct.Hct
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
import scheme.SchemeContent
|
||||||
|
import scheme.SchemeExpressive
|
||||||
|
import scheme.SchemeNeutral
|
||||||
|
import scheme.SchemeTonalSpot
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun lightColorSchemeOf(colors: ThemeColors): ColorScheme {
|
fun lightColorSchemeOf(colors: ThemeColors): ColorScheme {
|
||||||
return colorSchemeOf(colors.lightColorScheme.merge(DefaultLightColorScheme), colors.corePalette)
|
return colorSchemeOf(false, colors.lightColorScheme.merge(DefaultLightColorScheme), colors.corePalette)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun darkColorSchemeOf(colors: ThemeColors): ColorScheme {
|
fun darkColorSchemeOf(colors: ThemeColors): ColorScheme {
|
||||||
return colorSchemeOf(colors.darkColorScheme.merge(DefaultDarkColorScheme), colors.corePalette)
|
return colorSchemeOf(true, colors.darkColorScheme.merge(DefaultDarkColorScheme), colors.corePalette)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette): ColorScheme {
|
fun colorSchemeOf(dark: Boolean, colorScheme: FullColorScheme, corePalette: PartialCorePalette): ColorScheme {
|
||||||
val defaultPalette = systemCorePalette()
|
val defaultPalette = systemCorePalette(dark)
|
||||||
return remember(colorScheme, corePalette, defaultPalette) {
|
return remember(colorScheme, corePalette, defaultPalette) {
|
||||||
val mergedCorePalette = corePalette.merge(defaultPalette)
|
val mergedCorePalette = corePalette.merge(defaultPalette)
|
||||||
ColorScheme(
|
ColorScheme(
|
||||||
@@ -79,7 +85,7 @@ fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun systemCorePalette(): CorePalette<Int> {
|
fun systemCorePalette(dark: Boolean): CorePalette<Int> {
|
||||||
val uiSettings: UiSettings = koinInject()
|
val uiSettings: UiSettings = koinInject()
|
||||||
val compatModeColors by remember {
|
val compatModeColors by remember {
|
||||||
uiSettings.compatModeColors
|
uiSettings.compatModeColors
|
||||||
@@ -98,14 +104,20 @@ fun systemCorePalette(): CorePalette<Int> {
|
|||||||
}
|
}
|
||||||
val wallpaperColors = LocalWallpaperColors.current
|
val wallpaperColors = LocalWallpaperColors.current
|
||||||
return remember(wallpaperColors) {
|
return remember(wallpaperColors) {
|
||||||
val corePalette = palettes.CorePalette.of(wallpaperColors.primary.toArgb())
|
val corePalette = SchemeTonalSpot(
|
||||||
|
Hct.fromInt(
|
||||||
|
wallpaperColors.primary.toArgb()),
|
||||||
|
dark,
|
||||||
|
0.0,
|
||||||
|
ColorSpec.SpecVersion.SPEC_2025,
|
||||||
|
)
|
||||||
CorePalette(
|
CorePalette(
|
||||||
primary = corePalette.a1.tone(40),
|
primary = corePalette.primaryPaletteKeyColor,
|
||||||
secondary = corePalette.a2.tone(40),
|
secondary = corePalette.secondaryPaletteKeyColor,
|
||||||
tertiary = corePalette.a3.tone(40),
|
tertiary = corePalette.tertiaryPaletteKeyColor,
|
||||||
neutral = corePalette.n1.tone(40),
|
neutral = corePalette.neutralPaletteKeyColor,
|
||||||
neutralVariant = corePalette.n2.tone(40),
|
neutralVariant = corePalette.neutralVariantPaletteKeyColor,
|
||||||
error = corePalette.error.tone(40),
|
error = corePalette.errorPalette.keyColor.toInt(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ val DefaultDarkColorScheme = ColorScheme<Color>(
|
|||||||
onErrorContainer = ColorRef(CorePaletteColor.Error, 90),
|
onErrorContainer = ColorRef(CorePaletteColor.Error, 90),
|
||||||
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
|
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
|
||||||
surface = ColorRef(CorePaletteColor.Neutral, 6),
|
surface = ColorRef(CorePaletteColor.Neutral, 6),
|
||||||
surfaceBright = ColorRef(CorePaletteColor.Neutral, 24),
|
surfaceBright = ColorRef(CorePaletteColor.Neutral, 18),
|
||||||
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
|
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
|
||||||
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
|
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
|
||||||
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 12),
|
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 12),
|
||||||
@@ -136,7 +136,7 @@ val HighContrastDarkColorScheme = ColorScheme<Color>(
|
|||||||
onErrorContainer = ColorRef(CorePaletteColor.Error, 0),
|
onErrorContainer = ColorRef(CorePaletteColor.Error, 0),
|
||||||
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
|
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
|
||||||
surface = ColorRef(CorePaletteColor.Neutral, 6),
|
surface = ColorRef(CorePaletteColor.Neutral, 6),
|
||||||
surfaceBright = ColorRef(CorePaletteColor.Neutral, 24),
|
surfaceBright = ColorRef(CorePaletteColor.Neutral, 18),
|
||||||
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
|
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
|
||||||
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
|
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
|
||||||
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 12),
|
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 12),
|
||||||
|
|||||||
191
libs/material-color-utilities/src/main/java/LICENSE
Normal file
191
libs/material-color-utilities/src/main/java/LICENSE
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2025 Google LLC
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This file is automatically generated. Do not modify it.
|
|
||||||
|
|
||||||
package blend;
|
|
||||||
|
|
||||||
import hct.Cam16;
|
|
||||||
import hct.Hct;
|
|
||||||
import utils.ColorUtils;
|
|
||||||
import utils.MathUtils;
|
|
||||||
|
|
||||||
/** Functions for blending in HCT and CAM16. */
|
|
||||||
public class Blend {
|
|
||||||
private Blend() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blend the design color's HCT hue towards the key color's HCT hue, in a way that leaves the
|
|
||||||
* original color recognizable and recognizably shifted towards the key color.
|
|
||||||
*
|
|
||||||
* @param designColor ARGB representation of an arbitrary color.
|
|
||||||
* @param sourceColor ARGB representation of the main theme color.
|
|
||||||
* @return The design color with a hue shifted towards the system's color, a slightly
|
|
||||||
* warmer/cooler variant of the design color's hue.
|
|
||||||
*/
|
|
||||||
public static int harmonize(int designColor, int sourceColor) {
|
|
||||||
Hct fromHct = Hct.fromInt(designColor);
|
|
||||||
Hct toHct = Hct.fromInt(sourceColor);
|
|
||||||
double differenceDegrees = MathUtils.differenceDegrees(fromHct.getHue(), toHct.getHue());
|
|
||||||
double rotationDegrees = Math.min(differenceDegrees * 0.5, 15.0);
|
|
||||||
double outputHue =
|
|
||||||
MathUtils.sanitizeDegreesDouble(
|
|
||||||
fromHct.getHue()
|
|
||||||
+ rotationDegrees * MathUtils.rotationDirection(fromHct.getHue(), toHct.getHue()));
|
|
||||||
return Hct.from(outputHue, fromHct.getChroma(), fromHct.getTone()).toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blends hue from one color into another. The chroma and tone of the original color are
|
|
||||||
* maintained.
|
|
||||||
*
|
|
||||||
* @param from ARGB representation of color
|
|
||||||
* @param to ARGB representation of color
|
|
||||||
* @param amount how much blending to perform; 0.0 >= and <= 1.0
|
|
||||||
* @return from, with a hue blended towards to. Chroma and tone are constant.
|
|
||||||
*/
|
|
||||||
public static int hctHue(int from, int to, double amount) {
|
|
||||||
int ucs = cam16Ucs(from, to, amount);
|
|
||||||
Cam16 ucsCam = Cam16.fromInt(ucs);
|
|
||||||
Cam16 fromCam = Cam16.fromInt(from);
|
|
||||||
Hct blended = Hct.from(ucsCam.getHue(), fromCam.getChroma(), ColorUtils.lstarFromArgb(from));
|
|
||||||
return blended.toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blend in CAM16-UCS space.
|
|
||||||
*
|
|
||||||
* @param from ARGB representation of color
|
|
||||||
* @param to ARGB representation of color
|
|
||||||
* @param amount how much blending to perform; 0.0 >= and <= 1.0
|
|
||||||
* @return from, blended towards to. Hue, chroma, and tone will change.
|
|
||||||
*/
|
|
||||||
public static int cam16Ucs(int from, int to, double amount) {
|
|
||||||
Cam16 fromCam = Cam16.fromInt(from);
|
|
||||||
Cam16 toCam = Cam16.fromInt(to);
|
|
||||||
double fromJ = fromCam.getJstar();
|
|
||||||
double fromA = fromCam.getAstar();
|
|
||||||
double fromB = fromCam.getBstar();
|
|
||||||
double toJ = toCam.getJstar();
|
|
||||||
double toA = toCam.getAstar();
|
|
||||||
double toB = toCam.getBstar();
|
|
||||||
double jstar = fromJ + (toJ - fromJ) * amount;
|
|
||||||
double astar = fromA + (toA - fromA) * amount;
|
|
||||||
double bstar = fromB + (toB - fromB) * amount;
|
|
||||||
return Cam16.fromUcs(jstar, astar, bstar).toInt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
84
libs/material-color-utilities/src/main/java/blend/Blend.kt
Normal file
84
libs/material-color-utilities/src/main/java/blend/Blend.kt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package blend
|
||||||
|
|
||||||
|
import hct.Cam16
|
||||||
|
import hct.Hct
|
||||||
|
import utils.ColorUtils
|
||||||
|
import utils.MathUtils
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/** Functions for blending in HCT and CAM16. */
|
||||||
|
object Blend {
|
||||||
|
/**
|
||||||
|
* Blend the design color's HCT hue towards the key color's HCT hue, in a way that leaves the
|
||||||
|
* original color recognizable and recognizably shifted towards the key color.
|
||||||
|
*
|
||||||
|
* @param designColor ARGB representation of an arbitrary color.
|
||||||
|
* @param sourceColor ARGB representation of the main theme color.
|
||||||
|
* @return The design color with a hue shifted towards the system's color, a slightly
|
||||||
|
* warmer/cooler variant of the design color's hue.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun harmonize(designColor: Int, sourceColor: Int): Int {
|
||||||
|
val fromHct = Hct.fromInt(designColor)
|
||||||
|
val toHct = Hct.fromInt(sourceColor)
|
||||||
|
val differenceDegrees = MathUtils.differenceDegrees(fromHct.hue, toHct.hue)
|
||||||
|
val rotationDegrees = min(differenceDegrees * 0.5, 15.0)
|
||||||
|
val outputHue =
|
||||||
|
MathUtils.sanitizeDegreesDouble(
|
||||||
|
fromHct.hue + rotationDegrees * MathUtils.rotationDirection(fromHct.hue, toHct.hue)
|
||||||
|
)
|
||||||
|
return Hct.from(outputHue, fromHct.chroma, fromHct.tone).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blends hue from one color into another. The chroma and tone of the original color are
|
||||||
|
* maintained.
|
||||||
|
*
|
||||||
|
* @param from ARGB representation of color
|
||||||
|
* @param to ARGB representation of color
|
||||||
|
* @param amount how much blending to perform; 0.0 >= and <= 1.0
|
||||||
|
* @return from, with a hue blended towards to. Chroma and tone are constant.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun hctHue(from: Int, to: Int, amount: Double): Int {
|
||||||
|
val ucs = cam16Ucs(from, to, amount)
|
||||||
|
val ucsCam = Cam16.fromInt(ucs)
|
||||||
|
val fromCam = Cam16.fromInt(from)
|
||||||
|
val blended = Hct.from(ucsCam.hue, fromCam.chroma, ColorUtils.lstarFromArgb(from))
|
||||||
|
return blended.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blend in CAM16-UCS space.
|
||||||
|
*
|
||||||
|
* @param from ARGB representation of color
|
||||||
|
* @param to ARGB representation of color
|
||||||
|
* @param amount how much blending to perform; 0.0 >= and <= 1.0
|
||||||
|
* @return from, blended towards to. Hue, chroma, and tone will change.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun cam16Ucs(from: Int, to: Int, amount: Double): Int {
|
||||||
|
val fromCam = Cam16.fromInt(from)
|
||||||
|
val toCam = Cam16.fromInt(to)
|
||||||
|
val jstar = MathUtils.lerp(fromCam.jstar, toCam.jstar, amount)
|
||||||
|
val astar = MathUtils.lerp(fromCam.astar, toCam.astar, amount)
|
||||||
|
val bstar = MathUtils.lerp(fromCam.bstar, toCam.bstar, amount)
|
||||||
|
return Cam16.fromUcs(jstar, astar, bstar).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
214
libs/material-color-utilities/src/main/java/contrast/Contrast.kt
Normal file
214
libs/material-color-utilities/src/main/java/contrast/Contrast.kt
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package contrast
|
||||||
|
|
||||||
|
import utils.ColorUtils
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color science for contrast utilities.
|
||||||
|
*
|
||||||
|
* Utility methods for calculating contrast given two colors, or calculating a color given one color
|
||||||
|
* and a contrast ratio.
|
||||||
|
*
|
||||||
|
* Contrast ratio is calculated using XYZ's Y. When linearized to match human perception, Y becomes
|
||||||
|
* HCT's tone and L*a*b*'s' L*.
|
||||||
|
*/
|
||||||
|
object Contrast {
|
||||||
|
/**
|
||||||
|
* The minimum contrast ratio of two colors. Contrast ratio equation = (lighter + 5) / (darker +
|
||||||
|
* 5). If lighter == darker, ratio == 1.
|
||||||
|
*/
|
||||||
|
const val RATIO_MIN = 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum contrast ratio of two colors. Contrast ratio equation = (lighter + 5) / (darker +
|
||||||
|
* 5). If lighter == 100 and darker = 0, ratio == 21.
|
||||||
|
*/
|
||||||
|
const val RATIO_MAX = 21.0
|
||||||
|
const val RATIO_30 = 3.0
|
||||||
|
const val RATIO_45 = 4.5
|
||||||
|
const val RATIO_70 = 7.0
|
||||||
|
|
||||||
|
// Given a color and a contrast ratio to reach, the luminance of a color that reaches that ratio
|
||||||
|
// with the color can be calculated. However, that luminance may not contrast as desired, i.e. the
|
||||||
|
// contrast ratio of the input color and the returned luminance may not reach the contrast ratio
|
||||||
|
// asked for.
|
||||||
|
//
|
||||||
|
// When the desired contrast ratio and the result contrast ratio differ by more than this amount,
|
||||||
|
// an error value should be returned, or the method should be documented as 'unsafe', meaning,
|
||||||
|
// it will return a valid luminance but that luminance may not meet the requested contrast ratio.
|
||||||
|
//
|
||||||
|
// 0.04 selected because it ensures the resulting ratio rounds to the same tenth.
|
||||||
|
private const val CONTRAST_RATIO_EPSILON = 0.04
|
||||||
|
|
||||||
|
// Color spaces that measure luminance, such as Y in XYZ, L* in L*a*b*, or T in HCT, are known as
|
||||||
|
// perceptually accurate color spaces.
|
||||||
|
//
|
||||||
|
// To be displayed, they must gamut map to a "display space", one that has a defined limit on the
|
||||||
|
// number of colors. Display spaces include sRGB, more commonly understood as RGB/HSL/HSV/HSB.
|
||||||
|
// Gamut mapping is undefined and not defined by the color space. Any gamut mapping algorithm must
|
||||||
|
// choose how to sacrifice accuracy in hue, saturation, and/or lightness.
|
||||||
|
//
|
||||||
|
// A principled solution is to maintain lightness, thus maintaining contrast/a11y, maintain hue,
|
||||||
|
// thus maintaining aesthetic intent, and reduce chroma until the color is in gamut.
|
||||||
|
//
|
||||||
|
// HCT chooses this solution, but, that doesn't mean it will _exactly_ matched desired lightness,
|
||||||
|
// if only because RGB is quantized: RGB is expressed as a set of integers: there may be an RGB
|
||||||
|
// color with, for example, 47.892 lightness, but not 47.891.
|
||||||
|
//
|
||||||
|
// To allow for this inherent incompatibility between perceptually accurate color spaces and
|
||||||
|
// display color spaces, methods that take a contrast ratio and luminance, and return a luminance
|
||||||
|
// that reaches that contrast ratio for the input luminance, purposefully darken/lighten their
|
||||||
|
// result such that the desired contrast ratio will be reached even if inaccuracy is introduced.
|
||||||
|
//
|
||||||
|
// 0.4 is generous, ex. HCT requires much less delta. It was chosen because it provides a rough
|
||||||
|
// guarantee that as long as a perceptual color space gamut maps lightness such that the resulting
|
||||||
|
// lightness rounds to the same as the requested, the desired contrast ratio will be reached.
|
||||||
|
private const val LUMINANCE_GAMUT_MAP_TOLERANCE = 0.4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrast ratio is a measure of legibility, its used to compare the lightness of two colors.
|
||||||
|
* This method is used commonly in industry due to its use by WCAG.
|
||||||
|
*
|
||||||
|
* To compare lightness, the colors are expressed in the XYZ color space, where Y is lightness,
|
||||||
|
* also known as relative luminance.
|
||||||
|
*
|
||||||
|
* The equation is ratio = lighter Y + 5 / darker Y + 5.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun ratioOfYs(y1: Double, y2: Double): Double {
|
||||||
|
val lighter = max(y1, y2)
|
||||||
|
val darker = if (lighter == y2) y1 else y2
|
||||||
|
return (lighter + 5.0) / (darker + 5.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrast ratio of two tones. T in HCT, L* in L*a*b*. Also known as luminance or perpectual
|
||||||
|
* luminance.
|
||||||
|
*
|
||||||
|
* Contrast ratio is defined using Y in XYZ, relative luminance. However, relative luminance is
|
||||||
|
* linear to number of photons, not to perception of lightness. Perceptual luminance, L* in
|
||||||
|
* L*a*b*, T in HCT, is. Designers prefer color spaces with perceptual luminance since they're
|
||||||
|
* accurate to the eye.
|
||||||
|
*
|
||||||
|
* Y and L* are pure functions of each other, so it possible to use perceptually accurate color
|
||||||
|
* spaces, and measure contrast, and measure contrast in a much more understandable way: instead
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun ratioOfTones(t1: Double, t2: Double): Double {
|
||||||
|
return ratioOfYs(ColorUtils.yFromLstar(t1), ColorUtils.yFromLstar(t2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns T in HCT, L* in L*a*b* >= tone parameter that ensures ratio with input T/L*. Returns
|
||||||
|
* null if ratio cannot be achieved.
|
||||||
|
*
|
||||||
|
* @param tone Tone return value must contrast with.
|
||||||
|
* @param ratio Desired contrast ratio of return value and tone parameter.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun lighter(tone: Double, ratio: Double): Double? {
|
||||||
|
if (tone < 0.0 || tone > 100.0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Invert the contrast ratio equation to determine lighter Y given a ratio and darker Y.
|
||||||
|
val darkY = ColorUtils.yFromLstar(tone)
|
||||||
|
val lightY = ratio * (darkY + 5.0) - 5.0
|
||||||
|
if (lightY < 0.0 || lightY > 100.0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val realContrast = ratioOfYs(lightY, darkY)
|
||||||
|
val delta = abs(realContrast - ratio)
|
||||||
|
if (realContrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val returnValue = ColorUtils.lstarFromY(lightY) + LUMINANCE_GAMUT_MAP_TOLERANCE
|
||||||
|
// NOMUTANTS--important validation step; functions it is calling may change implementation.
|
||||||
|
return if (returnValue < 0.0 || returnValue > 100.0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
returnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun lighterUnsafe(tone: Double, ratio: Double): Double {
|
||||||
|
return lighter(tone, ratio) ?: 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns T in HCT, L* in L*a*b* <= tone parameter that ensures ratio with input T/L*. Returns
|
||||||
|
* null if ratio cannot be achieved.
|
||||||
|
*
|
||||||
|
* @param tone Tone return value must contrast with.
|
||||||
|
* @param ratio Desired contrast ratio of return value and tone parameter.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun darker(tone: Double, ratio: Double): Double? {
|
||||||
|
if (tone < 0.0 || tone > 100.0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Invert the contrast ratio equation to determine darker Y given a ratio and lighter Y.
|
||||||
|
val lightY = ColorUtils.yFromLstar(tone)
|
||||||
|
val darkY = (lightY + 5.0) / ratio - 5.0
|
||||||
|
if (darkY < 0.0 || darkY > 100.0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val realContrast = ratioOfYs(lightY, darkY)
|
||||||
|
val delta = abs(realContrast - ratio)
|
||||||
|
if (realContrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// For information on 0.4 constant, see comment in lighter(tone, ratio).
|
||||||
|
val returnValue = ColorUtils.lstarFromY(darkY) - LUMINANCE_GAMUT_MAP_TOLERANCE
|
||||||
|
// NOMUTANTS--important validation step; functions it is calling may change implementation.
|
||||||
|
return if (returnValue < 0.0 || returnValue > 100.0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
returnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun darkerUnsafe(tone: Double, ratio: Double): Double {
|
||||||
|
return darker(tone, ratio) ?: 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dislike
|
||||||
|
|
||||||
|
import hct.Hct
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and/or fix universally disliked colors.
|
||||||
|
*
|
||||||
|
* Color science studies of color preference indicate universal distaste for dark yellow-greens, and
|
||||||
|
* also show this is correlated to distate for biological waste and rotting food.
|
||||||
|
*
|
||||||
|
* See Palmer and Schloss, 2010 or Schloss and Palmer's Chapter 21 in Handbook of Color Psychology
|
||||||
|
* (2015).
|
||||||
|
*/
|
||||||
|
object DislikeAnalyzer {
|
||||||
|
/**
|
||||||
|
* Returns true if color is disliked.
|
||||||
|
*
|
||||||
|
* Disliked is defined as a dark yellow-green that is not neutral.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun isDisliked(hct: Hct): Boolean {
|
||||||
|
val huePasses = round(hct.hue) >= 90.0 && round(hct.hue) <= 111.0
|
||||||
|
val chromaPasses = round(hct.chroma) > 16.0
|
||||||
|
val tonePasses = round(hct.tone) < 65.0
|
||||||
|
return huePasses && chromaPasses && tonePasses
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If color is disliked, lighten it to make it likable. */
|
||||||
|
@JvmStatic
|
||||||
|
fun fixIfDisliked(hct: Hct): Hct =
|
||||||
|
if (isDisliked(hct)) {
|
||||||
|
Hct.from(hct.hue, hct.chroma, 70.0)
|
||||||
|
} else {
|
||||||
|
hct
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
import dynamiccolor.DynamicScheme.Platform
|
||||||
|
import hct.Hct
|
||||||
|
import palettes.TonalPalette
|
||||||
|
|
||||||
|
/** An interface defining all the necessary methods that could be different between specs. */
|
||||||
|
interface ColorSpec {
|
||||||
|
/** All available spec versions. */
|
||||||
|
enum class SpecVersion {
|
||||||
|
SPEC_2021,
|
||||||
|
SPEC_2025,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Main Palettes //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val primaryPaletteKeyColor: DynamicColor
|
||||||
|
val secondaryPaletteKeyColor: DynamicColor
|
||||||
|
val tertiaryPaletteKeyColor: DynamicColor
|
||||||
|
val neutralPaletteKeyColor: DynamicColor
|
||||||
|
val neutralVariantPaletteKeyColor: DynamicColor
|
||||||
|
val errorPaletteKeyColor: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Surfaces [S] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val background: DynamicColor
|
||||||
|
val onBackground: DynamicColor
|
||||||
|
val surface: DynamicColor
|
||||||
|
val surfaceDim: DynamicColor
|
||||||
|
val surfaceBright: DynamicColor
|
||||||
|
val surfaceContainerLowest: DynamicColor
|
||||||
|
val surfaceContainerLow: DynamicColor
|
||||||
|
val surfaceContainer: DynamicColor
|
||||||
|
val surfaceContainerHigh: DynamicColor
|
||||||
|
val surfaceContainerHighest: DynamicColor
|
||||||
|
val onSurface: DynamicColor
|
||||||
|
val surfaceVariant: DynamicColor
|
||||||
|
val onSurfaceVariant: DynamicColor
|
||||||
|
val inverseSurface: DynamicColor
|
||||||
|
val inverseOnSurface: DynamicColor
|
||||||
|
val outline: DynamicColor
|
||||||
|
val outlineVariant: DynamicColor
|
||||||
|
val shadow: DynamicColor
|
||||||
|
val scrim: DynamicColor
|
||||||
|
val surfaceTint: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Primaries [P] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val primary: DynamicColor
|
||||||
|
val primaryDim: DynamicColor?
|
||||||
|
val onPrimary: DynamicColor
|
||||||
|
val primaryContainer: DynamicColor
|
||||||
|
val onPrimaryContainer: DynamicColor
|
||||||
|
val inversePrimary: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Secondaries [Q] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val secondary: DynamicColor
|
||||||
|
val secondaryDim: DynamicColor?
|
||||||
|
val onSecondary: DynamicColor
|
||||||
|
val secondaryContainer: DynamicColor
|
||||||
|
val onSecondaryContainer: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Tertiaries [T] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val tertiary: DynamicColor
|
||||||
|
val tertiaryDim: DynamicColor?
|
||||||
|
val onTertiary: DynamicColor
|
||||||
|
val tertiaryContainer: DynamicColor
|
||||||
|
val onTertiaryContainer: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Errors [E] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val error: DynamicColor
|
||||||
|
val errorDim: DynamicColor?
|
||||||
|
val onError: DynamicColor
|
||||||
|
val errorContainer: DynamicColor
|
||||||
|
val onErrorContainer: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Primary Fixed Colors [PF] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val primaryFixed: DynamicColor
|
||||||
|
val primaryFixedDim: DynamicColor
|
||||||
|
val onPrimaryFixed: DynamicColor
|
||||||
|
val onPrimaryFixedVariant: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Secondary Fixed Colors [QF] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val secondaryFixed: DynamicColor
|
||||||
|
val secondaryFixedDim: DynamicColor
|
||||||
|
val onSecondaryFixed: DynamicColor
|
||||||
|
val onSecondaryFixedVariant: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Tertiary Fixed Colors [TF] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val tertiaryFixed: DynamicColor
|
||||||
|
val tertiaryFixedDim: DynamicColor
|
||||||
|
val onTertiaryFixed: DynamicColor
|
||||||
|
val onTertiaryFixedVariant: DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Other //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
fun highestSurface(scheme: DynamicScheme): DynamicColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Color value calculations //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
fun getHct(scheme: DynamicScheme, color: DynamicColor): Hct
|
||||||
|
|
||||||
|
fun getTone(scheme: DynamicScheme, color: DynamicColor): Double
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Scheme Palettes //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
fun getPrimaryPalette(
|
||||||
|
variant: Variant,
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
platform: Platform,
|
||||||
|
contrastLevel: Double,
|
||||||
|
): TonalPalette
|
||||||
|
|
||||||
|
fun getSecondaryPalette(
|
||||||
|
variant: Variant,
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
platform: Platform,
|
||||||
|
contrastLevel: Double,
|
||||||
|
): TonalPalette
|
||||||
|
|
||||||
|
fun getTertiaryPalette(
|
||||||
|
variant: Variant,
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
platform: Platform,
|
||||||
|
contrastLevel: Double,
|
||||||
|
): TonalPalette
|
||||||
|
|
||||||
|
fun getNeutralPalette(
|
||||||
|
variant: Variant,
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
platform: Platform,
|
||||||
|
contrastLevel: Double,
|
||||||
|
): TonalPalette
|
||||||
|
|
||||||
|
fun getNeutralVariantPalette(
|
||||||
|
variant: Variant,
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
platform: Platform,
|
||||||
|
contrastLevel: Double,
|
||||||
|
): TonalPalette
|
||||||
|
|
||||||
|
fun getErrorPalette(
|
||||||
|
variant: Variant,
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
platform: Platform,
|
||||||
|
contrastLevel: Double,
|
||||||
|
): TonalPalette
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
|
||||||
|
/** A utility class to get the correct color spec for a given spec version. */
|
||||||
|
object ColorSpecs {
|
||||||
|
private val SPEC_2021: ColorSpec = ColorSpec2021()
|
||||||
|
private val SPEC_2025: ColorSpec = ColorSpec2025()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun get(): ColorSpec {
|
||||||
|
return get(SpecVersion.SPEC_2021)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun get(specVersion: SpecVersion): ColorSpec {
|
||||||
|
return get(specVersion, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun get(specVersion: SpecVersion, isExtendedFidelity: Boolean): ColorSpec {
|
||||||
|
return if (specVersion == SpecVersion.SPEC_2025) SPEC_2025 else SPEC_2021
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
import utils.MathUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class containing a value that changes with the contrast level.
|
||||||
|
*
|
||||||
|
* Usually represents the contrast requirements for a dynamic color on its background. The four
|
||||||
|
* values correspond to values for contrast levels -1.0, 0.0, 0.5, and 1.0, respectively.
|
||||||
|
*
|
||||||
|
* @property low Value for contrast level -1.0
|
||||||
|
* @property normal Value for contrast level 0.0
|
||||||
|
* @property medium Value for contrast level 0.5
|
||||||
|
* @property high Value for contrast level 1.0
|
||||||
|
*/
|
||||||
|
class ContrastCurve(val low: Double, val normal: Double, val medium: Double, val high: Double) {
|
||||||
|
/**
|
||||||
|
* Returns the value at a given contrast level.
|
||||||
|
*
|
||||||
|
* @param contrastLevel The contrast level. 0.0 is the default (normal); -1.0 is the lowest; 1.0
|
||||||
|
* is the highest.
|
||||||
|
* @return The value. For contrast ratios, a number between 1.0 and 21.0.
|
||||||
|
*/
|
||||||
|
fun get(contrastLevel: Double): Double {
|
||||||
|
return when {
|
||||||
|
contrastLevel <= -1.0 -> low
|
||||||
|
contrastLevel < 0.0 -> MathUtils.lerp(low, normal, contrastLevel + 1.0)
|
||||||
|
contrastLevel < 0.5 -> MathUtils.lerp(normal, medium, contrastLevel / 0.5)
|
||||||
|
contrastLevel < 1.0 -> MathUtils.lerp(medium, high, (contrastLevel - 0.5) / 0.5)
|
||||||
|
else -> high
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
import contrast.Contrast
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import hct.Hct
|
||||||
|
import palettes.TonalPalette
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A color that adjusts itself based on UI state, represented by DynamicScheme.
|
||||||
|
*
|
||||||
|
* This color automatically adjusts to accommodate a desired contrast level, or other adjustments
|
||||||
|
* such as differing in light mode versus dark mode, or what the theme is, or what the color that
|
||||||
|
* produced the theme is, etc.
|
||||||
|
*
|
||||||
|
* Colors without backgrounds do not change tone when contrast changes. Colors with backgrounds
|
||||||
|
* become closer to their background as contrast lowers, and further when contrast increases.
|
||||||
|
*
|
||||||
|
* For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with its
|
||||||
|
* background is principled and matches accessibility guidance. That does not mean it's the desired
|
||||||
|
* approach for _every_ design system, and every color pairing, always, in every case.
|
||||||
|
*
|
||||||
|
* Ultimately, each component necessary for calculating a color, adjusting it for a desired contrast
|
||||||
|
* level, and ensuring it has a certain lightness/tone difference from another color, is provided by
|
||||||
|
* a function that takes a DynamicScheme and returns a value. This ensures ultimate flexibility, any
|
||||||
|
* desired behavior of a color for any design system, but is usually unnecessary.
|
||||||
|
*
|
||||||
|
* @param name The name of the dynamic color.
|
||||||
|
* @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
|
||||||
|
* defined by a hue and chroma, so this replaces the need to specify hue/chroma. By providing a
|
||||||
|
* tonal palette, when contrast adjustments are made, intended chroma can be preserved.
|
||||||
|
* @param isBackground Whether this dynamic color is a background, with some other color as the
|
||||||
|
* foreground.
|
||||||
|
* @param chromaMultiplier Function that provides a chroma multiplier, given a DynamicScheme.
|
||||||
|
* @param background Function that provides a background color, given a DynamicScheme.
|
||||||
|
* @param tone Function that provides a tone, given a DynamicScheme.
|
||||||
|
* @param secondBackground Function that provides a second background color, given a DynamicScheme.
|
||||||
|
* @param contrastCurve Function that provides a contrast curve, given a DynamicScheme.
|
||||||
|
* @param toneDeltaPair Function that provides a tone delta pair, given a DynamicScheme.
|
||||||
|
* @param opacity Function that provides an opacity percentage, given a DynamicScheme.
|
||||||
|
*/
|
||||||
|
data class DynamicColor(
|
||||||
|
val name: String,
|
||||||
|
val palette: (DynamicScheme) -> TonalPalette,
|
||||||
|
val isBackground: Boolean = false,
|
||||||
|
val chromaMultiplier: ((DynamicScheme) -> Double)? = null,
|
||||||
|
val background: ((DynamicScheme) -> DynamicColor?)? = null,
|
||||||
|
val tone: (DynamicScheme) -> Double = getInitialToneFromBackground(background),
|
||||||
|
val secondBackground: ((DynamicScheme) -> DynamicColor?)? = null,
|
||||||
|
val contrastCurve: ((DynamicScheme) -> ContrastCurve?)? = null,
|
||||||
|
val toneDeltaPair: ((DynamicScheme) -> ToneDeltaPair?)? = null,
|
||||||
|
val opacity: ((DynamicScheme) -> Double?)? = null,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
if (background == null && secondBackground != null) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Color $name has secondBackground defined, but background is not defined."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (background == null && contrastCurve != null) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Color $name has contrastCurve defined, but background is not defined."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (background != null && contrastCurve == null) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Color $name has background defined, but contrastCurve is not defined."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hctCache = mutableMapOf<DynamicScheme, Hct>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an ARGB integer (i.e. a hex code).
|
||||||
|
*
|
||||||
|
* @param scheme Defines the conditions of the user interface, for example, whether or not it is
|
||||||
|
* dark mode or light mode, and what the desired contrast level is.
|
||||||
|
*/
|
||||||
|
fun getArgb(scheme: DynamicScheme): Int {
|
||||||
|
val argb = getHct(scheme).toInt()
|
||||||
|
val opacityPercentage = opacity?.invoke(scheme)
|
||||||
|
return if (opacityPercentage == null) {
|
||||||
|
argb
|
||||||
|
} else {
|
||||||
|
val alpha = (opacityPercentage * 255).roundToInt().coerceIn(0, 255)
|
||||||
|
(argb and 0x00ffffff) or (alpha shl 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an HCT object.
|
||||||
|
*
|
||||||
|
* @param scheme Defines the conditions of the user interface, for example, whether or not it is
|
||||||
|
* dark mode or light mode, and what the desired contrast level is.
|
||||||
|
*/
|
||||||
|
fun getHct(scheme: DynamicScheme): Hct {
|
||||||
|
val cachedAnswer = hctCache[scheme]
|
||||||
|
if (cachedAnswer != null) {
|
||||||
|
return cachedAnswer
|
||||||
|
}
|
||||||
|
val answer = ColorSpecs.get(scheme.specVersion).getHct(scheme, this)
|
||||||
|
// NOMUTANTS--trivial test with onerous dependency injection requirement.
|
||||||
|
if (hctCache.size > 4) {
|
||||||
|
hctCache.clear()
|
||||||
|
}
|
||||||
|
// NOMUTANTS--trivial test with onerous dependency injection requirement.
|
||||||
|
hctCache[scheme] = answer
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the tone in HCT, ranging from 0 to 100, of the resolved color given scheme. */
|
||||||
|
fun getTone(scheme: DynamicScheme): Double {
|
||||||
|
return ColorSpecs.get(scheme.specVersion).getTone(scheme, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a DynamicColor from a hex code.
|
||||||
|
*
|
||||||
|
* Result has no background; thus no support for increasing/decreasing contrast for a11y.
|
||||||
|
*
|
||||||
|
* @param name The name of the dynamic color.
|
||||||
|
* @param argb The source color from which to extract the hue and chroma.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromArgb(name: String, argb: Int): DynamicColor {
|
||||||
|
val hct = Hct.fromInt(argb)
|
||||||
|
val palette = TonalPalette.fromInt(argb)
|
||||||
|
return DynamicColor(name = name, palette = { palette }, tone = { hct.tone })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio
|
||||||
|
* that is as close to ratio as possible.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun foregroundTone(bgTone: Double, ratio: Double): Double {
|
||||||
|
val lighterTone = Contrast.lighterUnsafe(bgTone, ratio)
|
||||||
|
val darkerTone = Contrast.darkerUnsafe(bgTone, ratio)
|
||||||
|
val lighterRatio = Contrast.ratioOfTones(lighterTone, bgTone)
|
||||||
|
val darkerRatio = Contrast.ratioOfTones(darkerTone, bgTone)
|
||||||
|
val preferLighter = tonePrefersLightForeground(bgTone)
|
||||||
|
if (preferLighter) {
|
||||||
|
// "Neglible difference" handles an edge case where the initial contrast ratio is high
|
||||||
|
// (ex. 13.0), and the ratio passed to the function is that high ratio, and both the lighter
|
||||||
|
// and darker ratio fails to pass that ratio.
|
||||||
|
//
|
||||||
|
// This was observed with Tonal Spot's On Primary Container turning black momentarily
|
||||||
|
// between
|
||||||
|
// high and max contrast in light mode. PC's standard tone was T90, OPC's was T10, it was
|
||||||
|
// light mode, and the contrast level was 0.6568521221032331.
|
||||||
|
val negligibleDifference =
|
||||||
|
abs(lighterRatio - darkerRatio) < 0.1 && lighterRatio < ratio && darkerRatio < ratio
|
||||||
|
return if (lighterRatio >= ratio || lighterRatio >= darkerRatio || negligibleDifference) {
|
||||||
|
lighterTone
|
||||||
|
} else {
|
||||||
|
darkerTone
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return if (darkerRatio >= ratio || darkerRatio >= lighterRatio) darkerTone else lighterTone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to
|
||||||
|
* supporting it.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun enableLightForeground(tone: Double): Double {
|
||||||
|
return if (tonePrefersLightForeground(tone) && !toneAllowsLightForeground(tone)) {
|
||||||
|
49.0
|
||||||
|
} else {
|
||||||
|
tone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* People prefer white foregrounds on ~T60-70. Observed over time, and also by Andrew Somers
|
||||||
|
* during research for APCA.
|
||||||
|
*
|
||||||
|
* T60 used as to create the smallest discontinuity possible when skipping down to T49 in order
|
||||||
|
* to ensure light foregrounds.
|
||||||
|
*
|
||||||
|
* Since `tertiaryContainer` in dark monochrome scheme requires a tone of 60, it should not be
|
||||||
|
* adjusted. Therefore, 60 is excluded here.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun tonePrefersLightForeground(tone: Double): Boolean {
|
||||||
|
return tone.roundToInt() < 60
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tones less than ~T50 always permit white at 4.5 contrast. */
|
||||||
|
@JvmStatic
|
||||||
|
fun toneAllowsLightForeground(tone: Double): Boolean {
|
||||||
|
return tone.roundToInt() <= 49
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getInitialToneFromBackground(
|
||||||
|
background: ((DynamicScheme) -> DynamicColor?)?
|
||||||
|
): (DynamicScheme) -> Double {
|
||||||
|
if (background == null) {
|
||||||
|
return { 50.0 }
|
||||||
|
}
|
||||||
|
return { scheme -> background(scheme)?.getTone(scheme) ?: 50.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DynamicColor.extendSpecVersion(
|
||||||
|
specVersion: SpecVersion,
|
||||||
|
extendedColor: DynamicColor,
|
||||||
|
): DynamicColor {
|
||||||
|
validateExtendedColor(specVersion, extendedColor)
|
||||||
|
return copy(
|
||||||
|
palette = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) extendedColor.palette else this.palette).invoke(
|
||||||
|
scheme
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tone = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) extendedColor.tone else this.tone).invoke(scheme)
|
||||||
|
},
|
||||||
|
chromaMultiplier = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) {
|
||||||
|
extendedColor.chromaMultiplier
|
||||||
|
} else {
|
||||||
|
this.chromaMultiplier
|
||||||
|
})
|
||||||
|
?.invoke(scheme) ?: 1.0
|
||||||
|
},
|
||||||
|
background = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) extendedColor.background else this.background)
|
||||||
|
?.invoke(scheme)
|
||||||
|
},
|
||||||
|
secondBackground = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) {
|
||||||
|
extendedColor.secondBackground
|
||||||
|
} else {
|
||||||
|
this.secondBackground
|
||||||
|
})
|
||||||
|
?.invoke(scheme)
|
||||||
|
},
|
||||||
|
contrastCurve = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) extendedColor.contrastCurve else this.contrastCurve)
|
||||||
|
?.invoke(scheme)
|
||||||
|
},
|
||||||
|
toneDeltaPair = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) extendedColor.toneDeltaPair else this.toneDeltaPair)
|
||||||
|
?.invoke(scheme)
|
||||||
|
},
|
||||||
|
opacity = { scheme ->
|
||||||
|
(if (scheme.specVersion == specVersion) extendedColor.opacity else this.opacity)?.invoke(
|
||||||
|
scheme
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DynamicColor.validateExtendedColor(
|
||||||
|
specVersion: SpecVersion,
|
||||||
|
extendedColor: DynamicColor,
|
||||||
|
) {
|
||||||
|
require(this.name == extendedColor.name) {
|
||||||
|
"Attempting to extend color $name with color ${extendedColor.name} of different name for spec version $specVersion."
|
||||||
|
}
|
||||||
|
require(this.isBackground == extendedColor.isBackground) {
|
||||||
|
"Attempting to extend color $name as a ${if (isBackground) "background" else "foreground"} with color ${extendedColor.name} as a ${if (extendedColor.isBackground) "background" else "foreground"} for spec version $specVersion."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import hct.Hct
|
||||||
|
import palettes.TonalPalette
|
||||||
|
import utils.MathUtils
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides important settings for creating colors dynamically, and 6 color palettes. Requires: 1. A
|
||||||
|
* color. (source color) 2. A theme. (Variant) 3. Whether or not its dark mode. 4. Contrast level.
|
||||||
|
* (-1 to 1, currently contrast ratio 3.0 and 7.0)
|
||||||
|
*/
|
||||||
|
open class DynamicScheme(
|
||||||
|
/** The source color of the scheme in HCT format. */
|
||||||
|
val sourceColorHct: Hct,
|
||||||
|
/** The variant of the scheme. */
|
||||||
|
val variant: Variant,
|
||||||
|
/** Whether or not the scheme is dark mode. */
|
||||||
|
val isDark: Boolean,
|
||||||
|
/**
|
||||||
|
* Value from -1 to 1. -1 represents minimum contrast. 0 represents standard (i.e. the design as
|
||||||
|
* spec'd), and 1 represents maximum contrast.
|
||||||
|
*/
|
||||||
|
val contrastLevel: Double,
|
||||||
|
/** The platform on which this scheme is intended to be used. */
|
||||||
|
val platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
/** The spec version of the scheme. */
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
val primaryPalette: TonalPalette,
|
||||||
|
val secondaryPalette: TonalPalette,
|
||||||
|
val tertiaryPalette: TonalPalette,
|
||||||
|
val neutralPalette: TonalPalette,
|
||||||
|
val neutralVariantPalette: TonalPalette,
|
||||||
|
val errorPalette: TonalPalette,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** The spec version of the scheme. */
|
||||||
|
val specVersion: SpecVersion
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.specVersion = maybeFallbackSpecVersion(specVersion, variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The source color of the scheme in ARGB format. */
|
||||||
|
val sourceColorArgb: Int = sourceColorHct.toInt()
|
||||||
|
|
||||||
|
/** The platform on which this scheme is intended to be used. */
|
||||||
|
enum class Platform {
|
||||||
|
PHONE,
|
||||||
|
WATCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHct(dynamicColor: DynamicColor): Hct {
|
||||||
|
return dynamicColor.getHct(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getArgb(dynamicColor: DynamicColor): Int {
|
||||||
|
return dynamicColor.getArgb(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Scheme: variant=${variant.name}, mode=${if (isDark) "dark" else "light"}, platform=${platform.name.lowercase(
|
||||||
|
Locale.ENGLISH
|
||||||
|
)}, contrastLevel=${DecimalFormat("0.0").format(contrastLevel)}, seed=$sourceColorHct, specVersion=$specVersion"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dynamicColors = MaterialDynamicColors()
|
||||||
|
|
||||||
|
val primaryPaletteKeyColor: Int
|
||||||
|
get() = getArgb(dynamicColors.primaryPaletteKeyColor)
|
||||||
|
|
||||||
|
val secondaryPaletteKeyColor: Int
|
||||||
|
get() = getArgb(dynamicColors.secondaryPaletteKeyColor)
|
||||||
|
|
||||||
|
val tertiaryPaletteKeyColor: Int
|
||||||
|
get() = getArgb(dynamicColors.tertiaryPaletteKeyColor)
|
||||||
|
|
||||||
|
val neutralPaletteKeyColor: Int
|
||||||
|
get() = getArgb(dynamicColors.neutralPaletteKeyColor)
|
||||||
|
|
||||||
|
val neutralVariantPaletteKeyColor: Int
|
||||||
|
get() = getArgb(dynamicColors.neutralVariantPaletteKeyColor)
|
||||||
|
|
||||||
|
val background: Int
|
||||||
|
get() = getArgb(dynamicColors.background)
|
||||||
|
|
||||||
|
val onBackground: Int
|
||||||
|
get() = getArgb(dynamicColors.onBackground)
|
||||||
|
|
||||||
|
val surface: Int
|
||||||
|
get() = getArgb(dynamicColors.surface)
|
||||||
|
|
||||||
|
val surfaceDim: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceDim)
|
||||||
|
|
||||||
|
val surfaceBright: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceBright)
|
||||||
|
|
||||||
|
val surfaceContainerLowest: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceContainerLowest)
|
||||||
|
|
||||||
|
val surfaceContainerLow: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceContainerLow)
|
||||||
|
|
||||||
|
val surfaceContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceContainer)
|
||||||
|
|
||||||
|
val surfaceContainerHigh: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceContainerHigh)
|
||||||
|
|
||||||
|
val surfaceContainerHighest: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceContainerHighest)
|
||||||
|
|
||||||
|
val onSurface: Int
|
||||||
|
get() = getArgb(dynamicColors.onSurface)
|
||||||
|
|
||||||
|
val surfaceVariant: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceVariant)
|
||||||
|
|
||||||
|
val onSurfaceVariant: Int
|
||||||
|
get() = getArgb(dynamicColors.onSurfaceVariant)
|
||||||
|
|
||||||
|
val inverseSurface: Int
|
||||||
|
get() = getArgb(dynamicColors.inverseSurface)
|
||||||
|
|
||||||
|
val inverseOnSurface: Int
|
||||||
|
get() = getArgb(dynamicColors.inverseOnSurface)
|
||||||
|
|
||||||
|
val outline: Int
|
||||||
|
get() = getArgb(dynamicColors.outline)
|
||||||
|
|
||||||
|
val outlineVariant: Int
|
||||||
|
get() = getArgb(dynamicColors.outlineVariant)
|
||||||
|
|
||||||
|
val shadow: Int
|
||||||
|
get() = getArgb(dynamicColors.shadow)
|
||||||
|
|
||||||
|
val scrim: Int
|
||||||
|
get() = getArgb(dynamicColors.scrim)
|
||||||
|
|
||||||
|
val surfaceTint: Int
|
||||||
|
get() = getArgb(dynamicColors.surfaceTint)
|
||||||
|
|
||||||
|
val primary: Int
|
||||||
|
get() = getArgb(dynamicColors.primary)
|
||||||
|
|
||||||
|
val onPrimary: Int
|
||||||
|
get() = getArgb(dynamicColors.onPrimary)
|
||||||
|
|
||||||
|
val primaryContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.primaryContainer)
|
||||||
|
|
||||||
|
val onPrimaryContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.onPrimaryContainer)
|
||||||
|
|
||||||
|
val inversePrimary: Int
|
||||||
|
get() = getArgb(dynamicColors.inversePrimary)
|
||||||
|
|
||||||
|
val secondary: Int
|
||||||
|
get() = getArgb(dynamicColors.secondary)
|
||||||
|
|
||||||
|
val onSecondary: Int
|
||||||
|
get() = getArgb(dynamicColors.onSecondary)
|
||||||
|
|
||||||
|
val secondaryContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.secondaryContainer)
|
||||||
|
|
||||||
|
val onSecondaryContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.onSecondaryContainer)
|
||||||
|
|
||||||
|
val tertiary: Int
|
||||||
|
get() = getArgb(dynamicColors.tertiary)
|
||||||
|
|
||||||
|
val onTertiary: Int
|
||||||
|
get() = getArgb(dynamicColors.onTertiary)
|
||||||
|
|
||||||
|
val tertiaryContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.tertiaryContainer)
|
||||||
|
|
||||||
|
val onTertiaryContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.onTertiaryContainer)
|
||||||
|
|
||||||
|
val error: Int
|
||||||
|
get() = getArgb(dynamicColors.error)
|
||||||
|
|
||||||
|
val onError: Int
|
||||||
|
get() = getArgb(dynamicColors.onError)
|
||||||
|
|
||||||
|
val errorContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.errorContainer)
|
||||||
|
|
||||||
|
val onErrorContainer: Int
|
||||||
|
get() = getArgb(dynamicColors.onErrorContainer)
|
||||||
|
|
||||||
|
val primaryFixed: Int
|
||||||
|
get() = getArgb(dynamicColors.primaryFixed)
|
||||||
|
|
||||||
|
val primaryFixedDim: Int
|
||||||
|
get() = getArgb(dynamicColors.primaryFixedDim)
|
||||||
|
|
||||||
|
val onPrimaryFixed: Int
|
||||||
|
get() = getArgb(dynamicColors.onPrimaryFixed)
|
||||||
|
|
||||||
|
val onPrimaryFixedVariant: Int
|
||||||
|
get() = getArgb(dynamicColors.onPrimaryFixedVariant)
|
||||||
|
|
||||||
|
val secondaryFixed: Int
|
||||||
|
get() = getArgb(dynamicColors.secondaryFixed)
|
||||||
|
|
||||||
|
val secondaryFixedDim: Int
|
||||||
|
get() = getArgb(dynamicColors.secondaryFixedDim)
|
||||||
|
|
||||||
|
val onSecondaryFixed: Int
|
||||||
|
get() = getArgb(dynamicColors.onSecondaryFixed)
|
||||||
|
|
||||||
|
val onSecondaryFixedVariant: Int
|
||||||
|
get() = getArgb(dynamicColors.onSecondaryFixedVariant)
|
||||||
|
|
||||||
|
val tertiaryFixed: Int
|
||||||
|
get() = getArgb(dynamicColors.tertiaryFixed)
|
||||||
|
|
||||||
|
val tertiaryFixedDim: Int
|
||||||
|
get() = getArgb(dynamicColors.tertiaryFixedDim)
|
||||||
|
|
||||||
|
val onTertiaryFixed: Int
|
||||||
|
get() = getArgb(dynamicColors.onTertiaryFixed)
|
||||||
|
|
||||||
|
val onTertiaryFixedVariant: Int
|
||||||
|
get() = getArgb(dynamicColors.onTertiaryFixedVariant)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_SPEC_VERSION = SpecVersion.SPEC_2021
|
||||||
|
val DEFAULT_PLATFORM = Platform.PHONE
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun from(other: DynamicScheme, isDark: Boolean): DynamicScheme {
|
||||||
|
return from(other, isDark, other.contrastLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun from(other: DynamicScheme, isDark: Boolean, contrastLevel: Double): DynamicScheme {
|
||||||
|
return DynamicScheme(
|
||||||
|
other.sourceColorHct,
|
||||||
|
other.variant,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
other.platform,
|
||||||
|
other.specVersion,
|
||||||
|
other.primaryPalette,
|
||||||
|
other.secondaryPalette,
|
||||||
|
other.tertiaryPalette,
|
||||||
|
other.neutralPalette,
|
||||||
|
other.neutralVariantPalette,
|
||||||
|
other.errorPalette,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new hue based on a piecewise function and input color hue.
|
||||||
|
*
|
||||||
|
* For example, for the following function:
|
||||||
|
* ```
|
||||||
|
* result = 26, if 0 <= hue < 101;
|
||||||
|
* result = 39, if 101 <= hue < 210;
|
||||||
|
* result = 28, if 210 <= hue < 360.
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* call the function as:
|
||||||
|
* ```
|
||||||
|
* double[] hueBreakpoints = {0, 101, 210, 360};
|
||||||
|
* double[] hues = {26, 39, 28};
|
||||||
|
* double result = scheme.piecewise(sourceColor, hueBreakpoints, hues);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param sourceColorHct The input value.
|
||||||
|
* @param hueBreakpoints The breakpoints, in sorted order. No default lower or upper bounds are
|
||||||
|
* assumed.
|
||||||
|
* @param hues The hues that should be applied when source color's hue is >= the same index in
|
||||||
|
* hueBreakpoints array, and < the hue at the next index in hueBreakpoints array. Otherwise,
|
||||||
|
* the source color's hue is returned.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getPiecewiseValue(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
hueBreakpoints: DoubleArray,
|
||||||
|
hues: DoubleArray,
|
||||||
|
): Double {
|
||||||
|
val size = min(hueBreakpoints.size - 1, hues.size)
|
||||||
|
val sourceHue = sourceColorHct.hue
|
||||||
|
for (i in 0 until size) {
|
||||||
|
if (sourceHue >= hueBreakpoints[i] && sourceHue < hueBreakpoints[i + 1]) {
|
||||||
|
return MathUtils.sanitizeDegreesDouble(hues[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No condition matched, return the source value.
|
||||||
|
return sourceHue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a shifted hue based on a piecewise function and input color hue.
|
||||||
|
*
|
||||||
|
* For example, for the following function:
|
||||||
|
* ```
|
||||||
|
* result = hue + 26, if 0 <= hue < 101;
|
||||||
|
* result = hue - 39, if 101 <= hue < 210;
|
||||||
|
* result = hue + 28, if 210 <= hue < 360.
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* call the function as:
|
||||||
|
* ```
|
||||||
|
* double[] hueBreakpoints = {0, 101, 210, 360};
|
||||||
|
* double[] rotations = {26, -39, 28};
|
||||||
|
* double result = scheme.getRotatedHue(sourceColor, hueBreakpoints, rotations);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param sourceColorHct the source color of the theme, in HCT.
|
||||||
|
* @param hueBreakpoints The "breakpoints", i.e. the hues at which a rotation should be apply.
|
||||||
|
* No default lower or upper bounds are assumed.
|
||||||
|
* @param rotations The rotation that should be applied when source color's hue is >= the same
|
||||||
|
* index in hues array, and < the hue at the next index in hues array. Otherwise, the source
|
||||||
|
* color's hue is returned.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getRotatedHue(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
hueBreakpoints: DoubleArray,
|
||||||
|
rotations: DoubleArray,
|
||||||
|
): Double {
|
||||||
|
var rotation = getPiecewiseValue(sourceColorHct, hueBreakpoints, rotations)
|
||||||
|
if (min(hueBreakpoints.size - 1, rotations.size) <= 0) {
|
||||||
|
// No condition matched, return the source hue.
|
||||||
|
rotation = 0.0
|
||||||
|
}
|
||||||
|
return MathUtils.sanitizeDegreesDouble(sourceColorHct.hue + rotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the spec version to use for the given variant. If the variant is not supported by the
|
||||||
|
* given spec version, the fallback spec version is returned.
|
||||||
|
*/
|
||||||
|
private fun maybeFallbackSpecVersion(specVersion: SpecVersion, variant: Variant): SpecVersion {
|
||||||
|
return when (variant) {
|
||||||
|
Variant.EXPRESSIVE,
|
||||||
|
Variant.VIBRANT,
|
||||||
|
Variant.TONAL_SPOT,
|
||||||
|
Variant.NEUTRAL -> specVersion
|
||||||
|
Variant.MONOCHROME,
|
||||||
|
Variant.FIDELITY,
|
||||||
|
Variant.CONTENT,
|
||||||
|
Variant.RAINBOW,
|
||||||
|
Variant.FRUIT_SALAD -> SpecVersion.SPEC_2021
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
/** Named colors, otherwise known as tokens, or roles, in the Material Design system. */
|
||||||
|
class MaterialDynamicColors {
|
||||||
|
private val colorSpec: ColorSpec = ColorSpec2025()
|
||||||
|
|
||||||
|
fun highestSurface(scheme: DynamicScheme): DynamicColor = colorSpec.highestSurface(scheme)
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Main Palettes //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val primaryPaletteKeyColor: DynamicColor
|
||||||
|
get() = colorSpec.primaryPaletteKeyColor
|
||||||
|
|
||||||
|
val secondaryPaletteKeyColor: DynamicColor
|
||||||
|
get() = colorSpec.secondaryPaletteKeyColor
|
||||||
|
|
||||||
|
val tertiaryPaletteKeyColor: DynamicColor
|
||||||
|
get() = colorSpec.tertiaryPaletteKeyColor
|
||||||
|
|
||||||
|
val neutralPaletteKeyColor: DynamicColor
|
||||||
|
get() = colorSpec.neutralPaletteKeyColor
|
||||||
|
|
||||||
|
val neutralVariantPaletteKeyColor: DynamicColor
|
||||||
|
get() = colorSpec.neutralVariantPaletteKeyColor
|
||||||
|
|
||||||
|
val errorPaletteKeyColor: DynamicColor
|
||||||
|
get() = colorSpec.errorPaletteKeyColor
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Surfaces [S] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val background: DynamicColor
|
||||||
|
get() = colorSpec.background
|
||||||
|
|
||||||
|
val onBackground: DynamicColor
|
||||||
|
get() = colorSpec.onBackground
|
||||||
|
|
||||||
|
val surface: DynamicColor
|
||||||
|
get() = colorSpec.surface
|
||||||
|
|
||||||
|
val surfaceDim: DynamicColor
|
||||||
|
get() = colorSpec.surfaceDim
|
||||||
|
|
||||||
|
val surfaceBright: DynamicColor
|
||||||
|
get() = colorSpec.surfaceBright
|
||||||
|
|
||||||
|
val surfaceContainerLowest: DynamicColor
|
||||||
|
get() = colorSpec.surfaceContainerLowest
|
||||||
|
|
||||||
|
val surfaceContainerLow: DynamicColor
|
||||||
|
get() = colorSpec.surfaceContainerLow
|
||||||
|
|
||||||
|
val surfaceContainer: DynamicColor
|
||||||
|
get() = colorSpec.surfaceContainer
|
||||||
|
|
||||||
|
val surfaceContainerHigh: DynamicColor
|
||||||
|
get() = colorSpec.surfaceContainerHigh
|
||||||
|
|
||||||
|
val surfaceContainerHighest: DynamicColor
|
||||||
|
get() = colorSpec.surfaceContainerHighest
|
||||||
|
|
||||||
|
val onSurface: DynamicColor
|
||||||
|
get() = colorSpec.onSurface
|
||||||
|
|
||||||
|
val surfaceVariant: DynamicColor
|
||||||
|
get() = colorSpec.surfaceVariant
|
||||||
|
|
||||||
|
val onSurfaceVariant: DynamicColor
|
||||||
|
get() = colorSpec.onSurfaceVariant
|
||||||
|
|
||||||
|
val inverseSurface: DynamicColor
|
||||||
|
get() = colorSpec.inverseSurface
|
||||||
|
|
||||||
|
val inverseOnSurface: DynamicColor
|
||||||
|
get() = colorSpec.inverseOnSurface
|
||||||
|
|
||||||
|
val outline: DynamicColor
|
||||||
|
get() = colorSpec.outline
|
||||||
|
|
||||||
|
val outlineVariant: DynamicColor
|
||||||
|
get() = colorSpec.outlineVariant
|
||||||
|
|
||||||
|
val shadow: DynamicColor
|
||||||
|
get() = colorSpec.shadow
|
||||||
|
|
||||||
|
val scrim: DynamicColor
|
||||||
|
get() = colorSpec.scrim
|
||||||
|
|
||||||
|
val surfaceTint: DynamicColor
|
||||||
|
get() = colorSpec.surfaceTint
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Primaries [P] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val primary: DynamicColor
|
||||||
|
get() = colorSpec.primary
|
||||||
|
|
||||||
|
val primaryDim: DynamicColor?
|
||||||
|
get() = colorSpec.primaryDim
|
||||||
|
|
||||||
|
val onPrimary: DynamicColor
|
||||||
|
get() = colorSpec.onPrimary
|
||||||
|
|
||||||
|
val primaryContainer: DynamicColor
|
||||||
|
get() = colorSpec.primaryContainer
|
||||||
|
|
||||||
|
val onPrimaryContainer: DynamicColor
|
||||||
|
get() = colorSpec.onPrimaryContainer
|
||||||
|
|
||||||
|
val inversePrimary: DynamicColor
|
||||||
|
get() = colorSpec.inversePrimary
|
||||||
|
|
||||||
|
// ///////////////////////////////////////////////////////////////
|
||||||
|
// Primary Fixed Colors [PF] //
|
||||||
|
// ///////////////////////////////////////////////////////////////
|
||||||
|
val primaryFixed: DynamicColor
|
||||||
|
get() = colorSpec.primaryFixed
|
||||||
|
|
||||||
|
val primaryFixedDim: DynamicColor
|
||||||
|
get() = colorSpec.primaryFixedDim
|
||||||
|
|
||||||
|
val onPrimaryFixed: DynamicColor
|
||||||
|
get() = colorSpec.onPrimaryFixed
|
||||||
|
|
||||||
|
val onPrimaryFixedVariant: DynamicColor
|
||||||
|
get() = colorSpec.onPrimaryFixedVariant
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Secondaries [Q] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val secondary: DynamicColor
|
||||||
|
get() = colorSpec.secondary
|
||||||
|
|
||||||
|
val secondaryDim: DynamicColor?
|
||||||
|
get() = colorSpec.secondaryDim
|
||||||
|
|
||||||
|
val onSecondary: DynamicColor
|
||||||
|
get() = colorSpec.onSecondary
|
||||||
|
|
||||||
|
val secondaryContainer: DynamicColor
|
||||||
|
get() = colorSpec.secondaryContainer
|
||||||
|
|
||||||
|
val onSecondaryContainer: DynamicColor
|
||||||
|
get() = colorSpec.onSecondaryContainer
|
||||||
|
|
||||||
|
// ///////////////////////////////////////////////////////////////
|
||||||
|
// Secondary Fixed Colors [QF] //
|
||||||
|
// ///////////////////////////////////////////////////////////////
|
||||||
|
val secondaryFixed: DynamicColor
|
||||||
|
get() = colorSpec.secondaryFixed
|
||||||
|
|
||||||
|
val secondaryFixedDim: DynamicColor
|
||||||
|
get() = colorSpec.secondaryFixedDim
|
||||||
|
|
||||||
|
val onSecondaryFixed: DynamicColor
|
||||||
|
get() = colorSpec.onSecondaryFixed
|
||||||
|
|
||||||
|
val onSecondaryFixedVariant: DynamicColor
|
||||||
|
get() = colorSpec.onSecondaryFixedVariant
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Tertiaries [T] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val tertiary: DynamicColor
|
||||||
|
get() = colorSpec.tertiary
|
||||||
|
|
||||||
|
val tertiaryDim: DynamicColor?
|
||||||
|
get() = colorSpec.tertiaryDim
|
||||||
|
|
||||||
|
val onTertiary: DynamicColor
|
||||||
|
get() = colorSpec.onTertiary
|
||||||
|
|
||||||
|
val tertiaryContainer: DynamicColor
|
||||||
|
get() = colorSpec.tertiaryContainer
|
||||||
|
|
||||||
|
val onTertiaryContainer: DynamicColor
|
||||||
|
get() = colorSpec.onTertiaryContainer
|
||||||
|
|
||||||
|
// ///////////////////////////////////////////////////////////////
|
||||||
|
// Tertiary Fixed Colors [TF] //
|
||||||
|
// ///////////////////////////////////////////////////////////////
|
||||||
|
val tertiaryFixed: DynamicColor
|
||||||
|
get() = colorSpec.tertiaryFixed
|
||||||
|
|
||||||
|
val tertiaryFixedDim: DynamicColor
|
||||||
|
get() = colorSpec.tertiaryFixedDim
|
||||||
|
|
||||||
|
val onTertiaryFixed: DynamicColor
|
||||||
|
get() = colorSpec.onTertiaryFixed
|
||||||
|
|
||||||
|
val onTertiaryFixedVariant: DynamicColor
|
||||||
|
get() = colorSpec.onTertiaryFixedVariant
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// Errors [E] //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
val error: DynamicColor
|
||||||
|
get() = colorSpec.error
|
||||||
|
|
||||||
|
val errorDim: DynamicColor?
|
||||||
|
get() = colorSpec.errorDim
|
||||||
|
|
||||||
|
val onError: DynamicColor
|
||||||
|
get() = colorSpec.onError
|
||||||
|
|
||||||
|
val errorContainer: DynamicColor
|
||||||
|
get() = colorSpec.errorContainer
|
||||||
|
|
||||||
|
val onErrorContainer: DynamicColor
|
||||||
|
get() = colorSpec.onErrorContainer
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
// All Colors //
|
||||||
|
// ////////////////////////////////////////////////////////////////
|
||||||
|
/** All dynamic colors in Material Design system. */
|
||||||
|
val allDynamicColors: List<() -> DynamicColor?> by lazy {
|
||||||
|
listOf(
|
||||||
|
this::primaryPaletteKeyColor,
|
||||||
|
this::secondaryPaletteKeyColor,
|
||||||
|
this::tertiaryPaletteKeyColor,
|
||||||
|
this::neutralPaletteKeyColor,
|
||||||
|
this::neutralVariantPaletteKeyColor,
|
||||||
|
this::errorPaletteKeyColor,
|
||||||
|
this::background,
|
||||||
|
this::onBackground,
|
||||||
|
this::surface,
|
||||||
|
this::surfaceDim,
|
||||||
|
this::surfaceBright,
|
||||||
|
this::surfaceContainerLowest,
|
||||||
|
this::surfaceContainerLow,
|
||||||
|
this::surfaceContainer,
|
||||||
|
this::surfaceContainerHigh,
|
||||||
|
this::surfaceContainerHighest,
|
||||||
|
this::onSurface,
|
||||||
|
this::surfaceVariant,
|
||||||
|
this::onSurfaceVariant,
|
||||||
|
this::outline,
|
||||||
|
this::outlineVariant,
|
||||||
|
this::inverseSurface,
|
||||||
|
this::inverseOnSurface,
|
||||||
|
this::shadow,
|
||||||
|
this::scrim,
|
||||||
|
this::surfaceTint,
|
||||||
|
this::primary,
|
||||||
|
this::primaryDim,
|
||||||
|
this::onPrimary,
|
||||||
|
this::primaryContainer,
|
||||||
|
this::onPrimaryContainer,
|
||||||
|
this::primaryFixed,
|
||||||
|
this::primaryFixedDim,
|
||||||
|
this::onPrimaryFixed,
|
||||||
|
this::onPrimaryFixedVariant,
|
||||||
|
this::inversePrimary,
|
||||||
|
this::secondary,
|
||||||
|
this::secondaryDim,
|
||||||
|
this::onSecondary,
|
||||||
|
this::secondaryContainer,
|
||||||
|
this::onSecondaryContainer,
|
||||||
|
this::secondaryFixed,
|
||||||
|
this::secondaryFixedDim,
|
||||||
|
this::onSecondaryFixed,
|
||||||
|
this::onSecondaryFixedVariant,
|
||||||
|
this::tertiary,
|
||||||
|
this::tertiaryDim,
|
||||||
|
this::onTertiary,
|
||||||
|
this::tertiaryContainer,
|
||||||
|
this::onTertiaryContainer,
|
||||||
|
this::tertiaryFixed,
|
||||||
|
this::tertiaryFixedDim,
|
||||||
|
this::onTertiaryFixed,
|
||||||
|
this::onTertiaryFixedVariant,
|
||||||
|
this::error,
|
||||||
|
this::errorDim,
|
||||||
|
this::onError,
|
||||||
|
this::errorContainer,
|
||||||
|
this::onErrorContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Documents a constraint between two DynamicColors, in which their tones must have a certain
|
||||||
|
* distance from each other.
|
||||||
|
*
|
||||||
|
* The polarity is an adjective that describes "A", compared to "B". For instance, ToneDeltaPair(A,
|
||||||
|
* B, 15, 'darker') states that A's tone should be at least 15 darker than B's.
|
||||||
|
*
|
||||||
|
* Prefer a DynamicColor with a background, this is for special cases when designers want tonal
|
||||||
|
* distance, literally contrast, between two colors that don't have a background / foreground
|
||||||
|
* relationship or a contrast guarantee.
|
||||||
|
*
|
||||||
|
* @param roleA The first role in a pair.
|
||||||
|
* @param roleB The second role in a pair.
|
||||||
|
* @param delta Required difference between tones. Absolute value, negative values have undefined
|
||||||
|
* behavior.
|
||||||
|
* @param polarity The relative relation between tones of roleA and roleB, as described above.
|
||||||
|
* @param stayTogether Whether these two roles should stay on the same side of the "awkward zone"
|
||||||
|
* (T50-59). This is necessary for certain cases where one role has two backgrounds.
|
||||||
|
* @param constraint How to fulfill the tone delta pair constraint.
|
||||||
|
*/
|
||||||
|
data class ToneDeltaPair(
|
||||||
|
val roleA: DynamicColor,
|
||||||
|
val roleB: DynamicColor,
|
||||||
|
val delta: Double,
|
||||||
|
val polarity: TonePolarity,
|
||||||
|
val stayTogether: Boolean = true,
|
||||||
|
val constraint: DeltaConstraint = DeltaConstraint.EXACT,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Describes how to fulfill a tone delta pair constraint.
|
||||||
|
*
|
||||||
|
* Determines if the delta is a minimum, maximum, or exact tonal distance that must be maintained.
|
||||||
|
*/
|
||||||
|
enum class DeltaConstraint {
|
||||||
|
// The tone of roleA must be an exact delta away from the tone of roleB.
|
||||||
|
EXACT,
|
||||||
|
// The tonal distance of roleA and roleB must be at most delta.
|
||||||
|
NEARER,
|
||||||
|
// The tonal distance of roleA and roleB must be at least delta.
|
||||||
|
FARTHER,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the relationship in lightness between two colors.
|
||||||
|
*
|
||||||
|
* 'relative_darker' and 'relative_lighter' describes the tone adjustment relative to the surface
|
||||||
|
* color trend (white in light mode; black in dark mode). For instance, ToneDeltaPair(A, B, 10,
|
||||||
|
* 'relative_lighter', 'farther') states that A should be at least 10 lighter than B in light
|
||||||
|
* mode, and at least 10 darker than B in dark mode.
|
||||||
|
*/
|
||||||
|
enum class TonePolarity {
|
||||||
|
// The tone of roleA is always darker than the tone of roleB.
|
||||||
|
DARKER,
|
||||||
|
// The tone of roleA is always lighter than the tone of roleB.
|
||||||
|
LIGHTER,
|
||||||
|
// The tone of roleA is darker than the tone of roleB in light mode, and lighter than the tone
|
||||||
|
// ofroleB in dark mode.
|
||||||
|
RELATIVE_DARKER,
|
||||||
|
// The tone of roleA is lighter than the tone of roleB in light mode, and darker than the tone
|
||||||
|
// ofroleB in dark mode.
|
||||||
|
RELATIVE_LIGHTER,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package dynamiccolor
|
||||||
|
|
||||||
|
/** Themes for Dynamic Color. */
|
||||||
|
enum class Variant {
|
||||||
|
MONOCHROME,
|
||||||
|
NEUTRAL,
|
||||||
|
TONAL_SPOT,
|
||||||
|
VIBRANT,
|
||||||
|
EXPRESSIVE,
|
||||||
|
FIDELITY,
|
||||||
|
CONTENT,
|
||||||
|
RAINBOW,
|
||||||
|
FRUIT_SALAD,
|
||||||
|
}
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package hct;
|
|
||||||
|
|
||||||
import static java.lang.Math.max;
|
|
||||||
|
|
||||||
import utils.ColorUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
|
|
||||||
* code and viewing conditions.
|
|
||||||
*
|
|
||||||
* <p>CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
|
|
||||||
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when
|
|
||||||
* measuring distances between colors.
|
|
||||||
*
|
|
||||||
* <p>In traditional color spaces, a color can be identified solely by the observer's measurement of
|
|
||||||
* the color. Color appearance models such as CAM16 also use information about the environment where
|
|
||||||
* the color was observed, known as the viewing conditions.
|
|
||||||
*
|
|
||||||
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
|
|
||||||
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
|
|
||||||
*/
|
|
||||||
public final class Cam16 {
|
|
||||||
// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
|
|
||||||
static final double[][] XYZ_TO_CAM16RGB = {
|
|
||||||
{0.401288, 0.650173, -0.051461},
|
|
||||||
{-0.250268, 1.204414, 0.045854},
|
|
||||||
{-0.002079, 0.048952, 0.953127}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
|
|
||||||
static final double[][] CAM16RGB_TO_XYZ = {
|
|
||||||
{1.8620678, -1.0112547, 0.14918678},
|
|
||||||
{0.38752654, 0.62144744, -0.00897398},
|
|
||||||
{-0.01584150, -0.03412294, 1.0499644}
|
|
||||||
};
|
|
||||||
|
|
||||||
// CAM16 color dimensions, see getters for documentation.
|
|
||||||
private final double hue;
|
|
||||||
private final double chroma;
|
|
||||||
private final double j;
|
|
||||||
private final double q;
|
|
||||||
private final double m;
|
|
||||||
private final double s;
|
|
||||||
|
|
||||||
// Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
|
|
||||||
private final double jstar;
|
|
||||||
private final double astar;
|
|
||||||
private final double bstar;
|
|
||||||
|
|
||||||
// Avoid allocations during conversion by pre-allocating an array.
|
|
||||||
private final double[] tempArray = new double[] {0.0, 0.0, 0.0};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
|
|
||||||
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
|
|
||||||
* distances between colors.
|
|
||||||
*/
|
|
||||||
double distance(Cam16 other) {
|
|
||||||
double dJ = getJstar() - other.getJstar();
|
|
||||||
double dA = getAstar() - other.getAstar();
|
|
||||||
double dB = getBstar() - other.getBstar();
|
|
||||||
double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
|
|
||||||
double dE = 1.41 * Math.pow(dEPrime, 0.63);
|
|
||||||
return dE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hue in CAM16 */
|
|
||||||
public double getHue() {
|
|
||||||
return hue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Chroma in CAM16 */
|
|
||||||
public double getChroma() {
|
|
||||||
return chroma;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lightness in CAM16 */
|
|
||||||
public double getJ() {
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Brightness in CAM16.
|
|
||||||
*
|
|
||||||
* <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is
|
|
||||||
* much brighter viewed in sunlight than in indoor light, but it is the lightest object under any
|
|
||||||
* lighting.
|
|
||||||
*/
|
|
||||||
public double getQ() {
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Colorfulness in CAM16.
|
|
||||||
*
|
|
||||||
* <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
|
|
||||||
* more colorful outside than inside, but it has the same chroma in both environments.
|
|
||||||
*/
|
|
||||||
public double getM() {
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saturation in CAM16.
|
|
||||||
*
|
|
||||||
* <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
|
|
||||||
* relative to the color's own brightness, where chroma is colorfulness relative to white.
|
|
||||||
*/
|
|
||||||
public double getS() {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lightness coordinate in CAM16-UCS */
|
|
||||||
public double getJstar() {
|
|
||||||
return jstar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** a* coordinate in CAM16-UCS */
|
|
||||||
public double getAstar() {
|
|
||||||
return astar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** b* coordinate in CAM16-UCS */
|
|
||||||
public double getBstar() {
|
|
||||||
return bstar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following
|
|
||||||
* combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static
|
|
||||||
* method that constructs from 3 of those dimensions. This constructor is intended for those
|
|
||||||
* methods to use to return all possible dimensions.
|
|
||||||
*
|
|
||||||
* @param hue for example, red, orange, yellow, green, etc.
|
|
||||||
* @param chroma informally, colorfulness / color intensity. like saturation in HSL, except
|
|
||||||
* perceptually accurate.
|
|
||||||
* @param j lightness
|
|
||||||
* @param q brightness; ratio of lightness to white point's lightness
|
|
||||||
* @param m colorfulness
|
|
||||||
* @param s saturation; ratio of chroma to white point's chroma
|
|
||||||
* @param jstar CAM16-UCS J coordinate
|
|
||||||
* @param astar CAM16-UCS a coordinate
|
|
||||||
* @param bstar CAM16-UCS b coordinate
|
|
||||||
*/
|
|
||||||
private Cam16(
|
|
||||||
double hue,
|
|
||||||
double chroma,
|
|
||||||
double j,
|
|
||||||
double q,
|
|
||||||
double m,
|
|
||||||
double s,
|
|
||||||
double jstar,
|
|
||||||
double astar,
|
|
||||||
double bstar) {
|
|
||||||
this.hue = hue;
|
|
||||||
this.chroma = chroma;
|
|
||||||
this.j = j;
|
|
||||||
this.q = q;
|
|
||||||
this.m = m;
|
|
||||||
this.s = s;
|
|
||||||
this.jstar = jstar;
|
|
||||||
this.astar = astar;
|
|
||||||
this.bstar = bstar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a CAM16 color from a color, assuming the color was viewed in default viewing conditions.
|
|
||||||
*
|
|
||||||
* @param argb ARGB representation of a color.
|
|
||||||
*/
|
|
||||||
public static Cam16 fromInt(int argb) {
|
|
||||||
return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a CAM16 color from a color in defined viewing conditions.
|
|
||||||
*
|
|
||||||
* @param argb ARGB representation of a color.
|
|
||||||
* @param viewingConditions Information about the environment where the color was observed.
|
|
||||||
*/
|
|
||||||
// The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
|
|
||||||
// may differ at runtime due to floating point imprecision, keeping the values the same, and
|
|
||||||
// accurate, across implementations takes precedence.
|
|
||||||
@SuppressWarnings("FloatingPointLiteralPrecision")
|
|
||||||
static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) {
|
|
||||||
// Transform ARGB int to XYZ
|
|
||||||
int red = (argb & 0x00ff0000) >> 16;
|
|
||||||
int green = (argb & 0x0000ff00) >> 8;
|
|
||||||
int blue = (argb & 0x000000ff);
|
|
||||||
double redL = ColorUtils.linearized(red);
|
|
||||||
double greenL = ColorUtils.linearized(green);
|
|
||||||
double blueL = ColorUtils.linearized(blue);
|
|
||||||
double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL;
|
|
||||||
double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
|
|
||||||
double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;
|
|
||||||
|
|
||||||
return fromXyzInViewingConditions(x, y, z, viewingConditions);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Cam16 fromXyzInViewingConditions(
|
|
||||||
double x, double y, double z, ViewingConditions viewingConditions) {
|
|
||||||
// Transform XYZ to 'cone'/'rgb' responses
|
|
||||||
double[][] matrix = XYZ_TO_CAM16RGB;
|
|
||||||
double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]);
|
|
||||||
double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]);
|
|
||||||
double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
|
|
||||||
|
|
||||||
// Discount illuminant
|
|
||||||
double rD = viewingConditions.getRgbD()[0] * rT;
|
|
||||||
double gD = viewingConditions.getRgbD()[1] * gT;
|
|
||||||
double bD = viewingConditions.getRgbD()[2] * bT;
|
|
||||||
|
|
||||||
// Chromatic adaptation
|
|
||||||
double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42);
|
|
||||||
double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42);
|
|
||||||
double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
|
|
||||||
double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13);
|
|
||||||
double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13);
|
|
||||||
double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13);
|
|
||||||
|
|
||||||
// redness-greenness
|
|
||||||
double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
|
|
||||||
// yellowness-blueness
|
|
||||||
double b = (rA + gA - 2.0 * bA) / 9.0;
|
|
||||||
|
|
||||||
// auxiliary components
|
|
||||||
double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0;
|
|
||||||
double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0;
|
|
||||||
|
|
||||||
// hue
|
|
||||||
double atan2 = Math.atan2(b, a);
|
|
||||||
double atanDegrees = Math.toDegrees(atan2);
|
|
||||||
double hue =
|
|
||||||
atanDegrees < 0
|
|
||||||
? atanDegrees + 360.0
|
|
||||||
: atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees;
|
|
||||||
double hueRadians = Math.toRadians(hue);
|
|
||||||
|
|
||||||
// achromatic response to color
|
|
||||||
double ac = p2 * viewingConditions.getNbb();
|
|
||||||
|
|
||||||
// CAM16 lightness and brightness
|
|
||||||
double j =
|
|
||||||
100.0
|
|
||||||
* Math.pow(
|
|
||||||
ac / viewingConditions.getAw(),
|
|
||||||
viewingConditions.getC() * viewingConditions.getZ());
|
|
||||||
double q =
|
|
||||||
4.0
|
|
||||||
/ viewingConditions.getC()
|
|
||||||
* Math.sqrt(j / 100.0)
|
|
||||||
* (viewingConditions.getAw() + 4.0)
|
|
||||||
* viewingConditions.getFlRoot();
|
|
||||||
|
|
||||||
// CAM16 chroma, colorfulness, and saturation.
|
|
||||||
double huePrime = (hue < 20.14) ? hue + 360 : hue;
|
|
||||||
double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
|
|
||||||
double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
|
|
||||||
double t = p1 * Math.hypot(a, b) / (u + 0.305);
|
|
||||||
double alpha =
|
|
||||||
Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9);
|
|
||||||
// CAM16 chroma, colorfulness, saturation
|
|
||||||
double c = alpha * Math.sqrt(j / 100.0);
|
|
||||||
double m = c * viewingConditions.getFlRoot();
|
|
||||||
double s =
|
|
||||||
50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
|
|
||||||
|
|
||||||
// CAM16-UCS components
|
|
||||||
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
|
|
||||||
double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
|
|
||||||
double astar = mstar * Math.cos(hueRadians);
|
|
||||||
double bstar = mstar * Math.sin(hueRadians);
|
|
||||||
|
|
||||||
return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param j CAM16 lightness
|
|
||||||
* @param c CAM16 chroma
|
|
||||||
* @param h CAM16 hue
|
|
||||||
*/
|
|
||||||
static Cam16 fromJch(double j, double c, double h) {
|
|
||||||
return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param j CAM16 lightness
|
|
||||||
* @param c CAM16 chroma
|
|
||||||
* @param h CAM16 hue
|
|
||||||
* @param viewingConditions Information about the environment where the color was observed.
|
|
||||||
*/
|
|
||||||
private static Cam16 fromJchInViewingConditions(
|
|
||||||
double j, double c, double h, ViewingConditions viewingConditions) {
|
|
||||||
double q =
|
|
||||||
4.0
|
|
||||||
/ viewingConditions.getC()
|
|
||||||
* Math.sqrt(j / 100.0)
|
|
||||||
* (viewingConditions.getAw() + 4.0)
|
|
||||||
* viewingConditions.getFlRoot();
|
|
||||||
double m = c * viewingConditions.getFlRoot();
|
|
||||||
double alpha = c / Math.sqrt(j / 100.0);
|
|
||||||
double s =
|
|
||||||
50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
|
|
||||||
|
|
||||||
double hueRadians = Math.toRadians(h);
|
|
||||||
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
|
|
||||||
double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
|
|
||||||
double astar = mstar * Math.cos(hueRadians);
|
|
||||||
double bstar = mstar * Math.sin(hueRadians);
|
|
||||||
return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a CAM16 color from CAM16-UCS coordinates.
|
|
||||||
*
|
|
||||||
* @param jstar CAM16-UCS lightness.
|
|
||||||
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
|
|
||||||
* axis.
|
|
||||||
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
|
|
||||||
* axis.
|
|
||||||
*/
|
|
||||||
public static Cam16 fromUcs(double jstar, double astar, double bstar) {
|
|
||||||
|
|
||||||
return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
|
|
||||||
*
|
|
||||||
* @param jstar CAM16-UCS lightness.
|
|
||||||
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
|
|
||||||
* axis.
|
|
||||||
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
|
|
||||||
* axis.
|
|
||||||
* @param viewingConditions Information about the environment where the color was observed.
|
|
||||||
*/
|
|
||||||
public static Cam16 fromUcsInViewingConditions(
|
|
||||||
double jstar, double astar, double bstar, ViewingConditions viewingConditions) {
|
|
||||||
|
|
||||||
double m = Math.hypot(astar, bstar);
|
|
||||||
double m2 = Math.expm1(m * 0.0228) / 0.0228;
|
|
||||||
double c = m2 / viewingConditions.getFlRoot();
|
|
||||||
double h = Math.atan2(bstar, astar) * (180.0 / Math.PI);
|
|
||||||
if (h < 0.0) {
|
|
||||||
h += 360.0;
|
|
||||||
}
|
|
||||||
double j = jstar / (1. - (jstar - 100.) * 0.007);
|
|
||||||
return fromJchInViewingConditions(j, c, h, viewingConditions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
|
|
||||||
* which are near-identical to the default viewing conditions for sRGB.
|
|
||||||
*/
|
|
||||||
public int toInt() {
|
|
||||||
return viewed(ViewingConditions.DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ARGB representation of the color, in defined viewing conditions.
|
|
||||||
*
|
|
||||||
* @param viewingConditions Information about the environment where the color will be viewed.
|
|
||||||
* @return ARGB representation of color
|
|
||||||
*/
|
|
||||||
int viewed(ViewingConditions viewingConditions) {
|
|
||||||
double[] xyz = xyzInViewingConditions(viewingConditions, tempArray);
|
|
||||||
return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
double[] xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray) {
|
|
||||||
double alpha =
|
|
||||||
(getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0);
|
|
||||||
|
|
||||||
double t =
|
|
||||||
Math.pow(
|
|
||||||
alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9);
|
|
||||||
double hRad = Math.toRadians(getHue());
|
|
||||||
|
|
||||||
double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
|
|
||||||
double ac =
|
|
||||||
viewingConditions.getAw()
|
|
||||||
* Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
|
|
||||||
double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
|
|
||||||
double p2 = (ac / viewingConditions.getNbb());
|
|
||||||
|
|
||||||
double hSin = Math.sin(hRad);
|
|
||||||
double hCos = Math.cos(hRad);
|
|
||||||
|
|
||||||
double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
|
|
||||||
double a = gamma * hCos;
|
|
||||||
double b = gamma * hSin;
|
|
||||||
double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
|
|
||||||
double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
|
|
||||||
double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
|
|
||||||
|
|
||||||
double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
|
|
||||||
double rC =
|
|
||||||
Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase, 1.0 / 0.42);
|
|
||||||
double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
|
|
||||||
double gC =
|
|
||||||
Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase, 1.0 / 0.42);
|
|
||||||
double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
|
|
||||||
double bC =
|
|
||||||
Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase, 1.0 / 0.42);
|
|
||||||
double rF = rC / viewingConditions.getRgbD()[0];
|
|
||||||
double gF = gC / viewingConditions.getRgbD()[1];
|
|
||||||
double bF = bC / viewingConditions.getRgbD()[2];
|
|
||||||
|
|
||||||
double[][] matrix = CAM16RGB_TO_XYZ;
|
|
||||||
double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
|
|
||||||
double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
|
|
||||||
double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
|
|
||||||
|
|
||||||
if (returnArray != null) {
|
|
||||||
returnArray[0] = x;
|
|
||||||
returnArray[1] = y;
|
|
||||||
returnArray[2] = z;
|
|
||||||
return returnArray;
|
|
||||||
} else {
|
|
||||||
return new double[] {x, y, z};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
366
libs/material-color-utilities/src/main/java/hct/Cam16.kt
Normal file
366
libs/material-color-utilities/src/main/java/hct/Cam16.kt
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package hct
|
||||||
|
|
||||||
|
import utils.ColorUtils
|
||||||
|
import utils.MathUtils
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.expm1
|
||||||
|
import kotlin.math.hypot
|
||||||
|
import kotlin.math.ln1p
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sign
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
|
||||||
|
* code and viewing conditions.
|
||||||
|
*
|
||||||
|
* CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar, astar,
|
||||||
|
* bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when
|
||||||
|
* measuring distances between colors.
|
||||||
|
*
|
||||||
|
* In traditional color spaces, a color can be identified solely by the observer's measurement of
|
||||||
|
* the color. Color appearance models such as CAM16 also use information about the environment where
|
||||||
|
* the color was observed, known as the viewing conditions.
|
||||||
|
*
|
||||||
|
* For example, white under the traditional assumption of a midday sun white point is accurately
|
||||||
|
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
|
||||||
|
*/
|
||||||
|
@ConsistentCopyVisibility
|
||||||
|
data class Cam16
|
||||||
|
private constructor(
|
||||||
|
/** Hue in CAM16 */
|
||||||
|
val hue: Double,
|
||||||
|
/** Chroma in CAM16 */
|
||||||
|
val chroma: Double,
|
||||||
|
/** Lightness in CAM16 */
|
||||||
|
val j: Double,
|
||||||
|
/**
|
||||||
|
* Brightness in CAM16.
|
||||||
|
*
|
||||||
|
* Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is
|
||||||
|
* much brighter viewed in sunlight than in indoor light, but it is the lightest object under any
|
||||||
|
* lighting.
|
||||||
|
*/
|
||||||
|
val q: Double,
|
||||||
|
/**
|
||||||
|
* Colorfulness in CAM16.
|
||||||
|
*
|
||||||
|
* Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much more
|
||||||
|
* colorful outside than inside, but it has the same chroma in both environments.
|
||||||
|
*/
|
||||||
|
val m: Double,
|
||||||
|
/**
|
||||||
|
* Saturation in CAM16.
|
||||||
|
*
|
||||||
|
* Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
|
||||||
|
* relative to the color's own brightness, where chroma is colorfulness relative to white.
|
||||||
|
*/
|
||||||
|
val s: Double,
|
||||||
|
/** Lightness coordinate in CAM16-UCS */
|
||||||
|
val jstar: Double,
|
||||||
|
/** a* coordinate in CAM16-UCS */
|
||||||
|
val astar: Double,
|
||||||
|
/** b* coordinate in CAM16-UCS */
|
||||||
|
val bstar: Double,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
|
||||||
|
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
|
||||||
|
* distances between colors.
|
||||||
|
*/
|
||||||
|
fun distance(other: Cam16): Double {
|
||||||
|
val dJ = jstar - other.jstar
|
||||||
|
val dA = astar - other.astar
|
||||||
|
val dB = bstar - other.bstar
|
||||||
|
val dEPrime = sqrt(dJ * dJ + dA * dA + dB * dB)
|
||||||
|
val dE = 1.41 * dEPrime.pow(0.63)
|
||||||
|
return dE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
|
||||||
|
* which are near-identical to the default viewing conditions for sRGB.
|
||||||
|
*/
|
||||||
|
fun toInt(): Int {
|
||||||
|
return viewed(ViewingConditions.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ARGB representation of the color, in defined viewing conditions.
|
||||||
|
*
|
||||||
|
* @param viewingConditions Information about the environment where the color will be viewed.
|
||||||
|
* @return ARGB representation of color
|
||||||
|
*/
|
||||||
|
internal fun viewed(viewingConditions: ViewingConditions): Int {
|
||||||
|
val xyz = xyzInViewingConditions(viewingConditions)
|
||||||
|
return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun xyzInViewingConditions(
|
||||||
|
viewingConditions: ViewingConditions,
|
||||||
|
returnArray: DoubleArray? = null,
|
||||||
|
): DoubleArray {
|
||||||
|
val alpha = if (chroma == 0.0 || j == 0.0) 0.0 else chroma / sqrt(j / 100.0)
|
||||||
|
val t = (alpha / (1.64 - 0.29.pow(viewingConditions.n)).pow(0.73)).pow(1.0 / 0.9)
|
||||||
|
val hRad = Math.toRadians(hue)
|
||||||
|
val eHue = 0.25 * (cos(hRad + 2.0) + 3.8)
|
||||||
|
val ac = viewingConditions.aw * (j / 100.0).pow(1.0 / viewingConditions.c / viewingConditions.z)
|
||||||
|
val p1 = eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb
|
||||||
|
val p2 = ac / viewingConditions.nbb
|
||||||
|
val hSin = sin(hRad)
|
||||||
|
val hCos = cos(hRad)
|
||||||
|
val gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin)
|
||||||
|
val a = gamma * hCos
|
||||||
|
val b = gamma * hSin
|
||||||
|
val rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0
|
||||||
|
val gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0
|
||||||
|
val bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0
|
||||||
|
val rCBase = max(0.0, 27.13 * abs(rA) / (400.0 - abs(rA)))
|
||||||
|
val rC = sign(rA) * (100.0 / viewingConditions.fl) * (rCBase).pow(1.0 / 0.42)
|
||||||
|
val gCBase = max(0.0, 27.13 * abs(gA) / (400.0 - abs(gA)))
|
||||||
|
val gC = sign(gA) * (100.0 / viewingConditions.fl) * (gCBase).pow(1.0 / 0.42)
|
||||||
|
val bCBase = max(0.0, 27.13 * abs(bA) / (400.0 - abs(bA)))
|
||||||
|
val bC = sign(bA) * (100.0 / viewingConditions.fl) * (bCBase).pow(1.0 / 0.42)
|
||||||
|
val rF = rC / viewingConditions.rgbD[0]
|
||||||
|
val gF = gC / viewingConditions.rgbD[1]
|
||||||
|
val bF = bC / viewingConditions.rgbD[2]
|
||||||
|
val matrix = CAM16RGB_TO_XYZ
|
||||||
|
val x = rF * matrix[0][0] + gF * matrix[0][1] + bF * matrix[0][2]
|
||||||
|
val y = rF * matrix[1][0] + gF * matrix[1][1] + bF * matrix[1][2]
|
||||||
|
val z = rF * matrix[2][0] + gF * matrix[2][1] + bF * matrix[2][2]
|
||||||
|
return if (returnArray != null) {
|
||||||
|
returnArray[0] = x
|
||||||
|
returnArray[1] = y
|
||||||
|
returnArray[2] = z
|
||||||
|
returnArray
|
||||||
|
} else {
|
||||||
|
doubleArrayOf(x, y, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
|
||||||
|
internal val XYZ_TO_CAM16RGB =
|
||||||
|
arrayOf(
|
||||||
|
doubleArrayOf(0.401288, 0.650173, -0.051461),
|
||||||
|
doubleArrayOf(-0.250268, 1.204414, 0.045854),
|
||||||
|
doubleArrayOf(-0.002079, 0.048952, 0.953127),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
|
||||||
|
internal val CAM16RGB_TO_XYZ =
|
||||||
|
arrayOf(
|
||||||
|
doubleArrayOf(1.8620678, -1.0112547, 0.14918678),
|
||||||
|
doubleArrayOf(0.38752654, 0.62144744, -0.00897398),
|
||||||
|
doubleArrayOf(-0.0158415, -0.03412294, 1.0499644),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CAM16 color from a color, assuming the color was viewed in default viewing
|
||||||
|
* conditions.
|
||||||
|
*
|
||||||
|
* @param argb ARGB representation of a color.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromInt(argb: Int): Cam16 {
|
||||||
|
return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CAM16 color from a color in defined viewing conditions.
|
||||||
|
*
|
||||||
|
* @param argb ARGB representation of a color.
|
||||||
|
* @param viewingConditions Information about the environment where the color was observed.
|
||||||
|
*/
|
||||||
|
// The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
|
||||||
|
// may differ at runtime due to floating point imprecision, keeping the values the same, and
|
||||||
|
// accurate, across implementations takes precedence.
|
||||||
|
@SuppressWarnings("FloatingPointLiteralPrecision")
|
||||||
|
internal fun fromIntInViewingConditions(
|
||||||
|
argb: Int,
|
||||||
|
viewingConditions: ViewingConditions,
|
||||||
|
): Cam16 {
|
||||||
|
// Transform ARGB int to XYZ
|
||||||
|
val red = argb and 0x00ff0000 shr 16
|
||||||
|
val green = argb and 0x0000ff00 shr 8
|
||||||
|
val blue = argb and 0x000000ff
|
||||||
|
val redL = ColorUtils.linearized(red)
|
||||||
|
val greenL = ColorUtils.linearized(green)
|
||||||
|
val blueL = ColorUtils.linearized(blue)
|
||||||
|
val x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL
|
||||||
|
val y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL
|
||||||
|
val z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL
|
||||||
|
return fromXyzInViewingConditions(x, y, z, viewingConditions)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun fromXyzInViewingConditions(
|
||||||
|
x: Double,
|
||||||
|
y: Double,
|
||||||
|
z: Double,
|
||||||
|
viewingConditions: ViewingConditions,
|
||||||
|
): Cam16 {
|
||||||
|
// Transform XYZ to 'cone'/'rgb' responses
|
||||||
|
val matrix = XYZ_TO_CAM16RGB
|
||||||
|
val rT = x * matrix[0][0] + y * matrix[0][1] + z * matrix[0][2]
|
||||||
|
val gT = x * matrix[1][0] + y * matrix[1][1] + z * matrix[1][2]
|
||||||
|
val bT = x * matrix[2][0] + y * matrix[2][1] + z * matrix[2][2]
|
||||||
|
|
||||||
|
// Discount illuminant
|
||||||
|
val rD = viewingConditions.rgbD[0] * rT
|
||||||
|
val gD = viewingConditions.rgbD[1] * gT
|
||||||
|
val bD = viewingConditions.rgbD[2] * bT
|
||||||
|
|
||||||
|
// Chromatic adaptation
|
||||||
|
val rAF = (viewingConditions.fl * abs(rD) / 100.0).pow(0.42)
|
||||||
|
val gAF = (viewingConditions.fl * abs(gD) / 100.0).pow(0.42)
|
||||||
|
val bAF = (viewingConditions.fl * abs(bD) / 100.0).pow(0.42)
|
||||||
|
val rA = sign(rD) * 400.0 * rAF / (rAF + 27.13)
|
||||||
|
val gA = sign(gD) * 400.0 * gAF / (gAF + 27.13)
|
||||||
|
val bA = sign(bD) * 400.0 * bAF / (bAF + 27.13)
|
||||||
|
|
||||||
|
// redness-greenness
|
||||||
|
val a = (11.0 * rA + -12.0 * gA + bA) / 11.0
|
||||||
|
// yellowness-blueness
|
||||||
|
val b = (rA + gA - 2.0 * bA) / 9.0
|
||||||
|
|
||||||
|
// auxiliary components
|
||||||
|
val u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0
|
||||||
|
val p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0
|
||||||
|
|
||||||
|
// hue
|
||||||
|
val atan2 = atan2(b, a)
|
||||||
|
val atanDegrees = Math.toDegrees(atan2)
|
||||||
|
val hue = MathUtils.sanitizeDegreesDouble(atanDegrees)
|
||||||
|
val hueRadians = Math.toRadians(hue)
|
||||||
|
|
||||||
|
// achromatic response to color
|
||||||
|
val ac = p2 * viewingConditions.nbb
|
||||||
|
|
||||||
|
// CAM16 lightness and brightness
|
||||||
|
val j = 100.0 * (ac / viewingConditions.aw).pow(viewingConditions.c * viewingConditions.z)
|
||||||
|
val q =
|
||||||
|
4.0 / viewingConditions.c *
|
||||||
|
sqrt(j / 100.0) *
|
||||||
|
(viewingConditions.aw + 4.0) *
|
||||||
|
viewingConditions.flRoot
|
||||||
|
|
||||||
|
// CAM16 chroma, colorfulness, and saturation.
|
||||||
|
val huePrime = if (hue < 20.14) hue + 360 else hue
|
||||||
|
val eHue = 0.25 * (cos(Math.toRadians(huePrime) + 2.0) + 3.8)
|
||||||
|
val p1 = 50000.0 / 13.0 * eHue * viewingConditions.nc * viewingConditions.ncb
|
||||||
|
val t = p1 * hypot(a, b) / (u + 0.305)
|
||||||
|
val alpha = (1.64 - 0.29.pow(viewingConditions.n)).pow(0.73) * t.pow(0.9)
|
||||||
|
// CAM16 chroma, colorfulness, saturation
|
||||||
|
val c = alpha * sqrt(j / 100.0)
|
||||||
|
val m = c * viewingConditions.flRoot
|
||||||
|
val s = 50.0 * sqrt(alpha * viewingConditions.c / (viewingConditions.aw + 4.0))
|
||||||
|
|
||||||
|
// CAM16-UCS components
|
||||||
|
val jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j)
|
||||||
|
val mstar = 1.0 / 0.0228 * ln1p(0.0228 * m)
|
||||||
|
val astar = mstar * cos(hueRadians)
|
||||||
|
val bstar = mstar * sin(hueRadians)
|
||||||
|
return Cam16(hue, c, j, q, m, s, jstar, astar, bstar)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param j CAM16 lightness
|
||||||
|
* @param c CAM16 chroma
|
||||||
|
* @param h CAM16 hue
|
||||||
|
*/
|
||||||
|
internal fun fromJch(j: Double, c: Double, h: Double): Cam16 {
|
||||||
|
return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param j CAM16 lightness
|
||||||
|
* @param c CAM16 chroma
|
||||||
|
* @param h CAM16 hue
|
||||||
|
* @param viewingConditions Information about the environment where the color was observed.
|
||||||
|
*/
|
||||||
|
private fun fromJchInViewingConditions(
|
||||||
|
j: Double,
|
||||||
|
c: Double,
|
||||||
|
h: Double,
|
||||||
|
viewingConditions: ViewingConditions,
|
||||||
|
): Cam16 {
|
||||||
|
val q =
|
||||||
|
4.0 / viewingConditions.c *
|
||||||
|
sqrt(j / 100.0) *
|
||||||
|
(viewingConditions.aw + 4.0) *
|
||||||
|
viewingConditions.flRoot
|
||||||
|
val m = c * viewingConditions.flRoot
|
||||||
|
val alpha = c / sqrt(j / 100.0)
|
||||||
|
val s = 50.0 * sqrt(alpha * viewingConditions.c / (viewingConditions.aw + 4.0))
|
||||||
|
val hueRadians = Math.toRadians(h)
|
||||||
|
val jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j)
|
||||||
|
val mstar = 1.0 / 0.0228 * ln1p(0.0228 * m)
|
||||||
|
val astar = mstar * cos(hueRadians)
|
||||||
|
val bstar = mstar * sin(hueRadians)
|
||||||
|
return Cam16(h, c, j, q, m, s, jstar, astar, bstar)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CAM16 color from CAM16-UCS coordinates.
|
||||||
|
*
|
||||||
|
* @param jstar CAM16-UCS lightness.
|
||||||
|
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
|
||||||
|
* axis.
|
||||||
|
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
|
||||||
|
* axis.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromUcs(jstar: Double, astar: Double, bstar: Double): Cam16 {
|
||||||
|
return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
|
||||||
|
*
|
||||||
|
* @param jstar CAM16-UCS lightness.
|
||||||
|
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
|
||||||
|
* axis.
|
||||||
|
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
|
||||||
|
* axis.
|
||||||
|
* @param viewingConditions Information about the environment where the color was observed.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromUcsInViewingConditions(
|
||||||
|
jstar: Double,
|
||||||
|
astar: Double,
|
||||||
|
bstar: Double,
|
||||||
|
viewingConditions: ViewingConditions,
|
||||||
|
): Cam16 {
|
||||||
|
val m = hypot(astar, bstar)
|
||||||
|
val m2 = expm1(m * 0.0228) / 0.0228
|
||||||
|
val c = m2 / viewingConditions.flRoot
|
||||||
|
var h = atan2(bstar, astar) * (180.0 / PI)
|
||||||
|
if (h < 0.0) {
|
||||||
|
h += 360.0
|
||||||
|
}
|
||||||
|
val j = jstar / (1.0 - (jstar - 100.0) * 0.007)
|
||||||
|
return fromJchInViewingConditions(j, c, h, viewingConditions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package hct;
|
|
||||||
|
|
||||||
import utils.ColorUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A color system built using CAM16 hue and chroma, and L* from L*a*b*.
|
|
||||||
*
|
|
||||||
* <p>Using L* creates a link between the color system, contrast, and thus accessibility. Contrast
|
|
||||||
* ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can
|
|
||||||
* be calculated from Y.
|
|
||||||
*
|
|
||||||
* <p>Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
|
|
||||||
*
|
|
||||||
* <p>Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A
|
|
||||||
* difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50
|
|
||||||
* guarantees a contrast ratio >= 4.5.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color
|
|
||||||
* measurement system that can also accurately render what colors will appear as in different
|
|
||||||
* lighting environments.
|
|
||||||
*/
|
|
||||||
public final class Hct {
|
|
||||||
private double hue;
|
|
||||||
private double chroma;
|
|
||||||
private double tone;
|
|
||||||
private int argb;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HCT color from hue, chroma, and tone.
|
|
||||||
*
|
|
||||||
* @param hue 0 <= hue < 360; invalid values are corrected.
|
|
||||||
* @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower than
|
|
||||||
* the requested chroma. Chroma has a different maximum for any given hue and tone.
|
|
||||||
* @param tone 0 <= tone <= 100; invalid values are corrected.
|
|
||||||
* @return HCT representation of a color in default viewing conditions.
|
|
||||||
*/
|
|
||||||
public static Hct from(double hue, double chroma, double tone) {
|
|
||||||
int argb = HctSolver.solveToInt(hue, chroma, tone);
|
|
||||||
return new Hct(argb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HCT color from a color.
|
|
||||||
*
|
|
||||||
* @param argb ARGB representation of a color.
|
|
||||||
* @return HCT representation of a color in default viewing conditions
|
|
||||||
*/
|
|
||||||
public static Hct fromInt(int argb) {
|
|
||||||
return new Hct(argb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Hct(int argb) {
|
|
||||||
setInternalState(argb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getHue() {
|
|
||||||
return hue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getChroma() {
|
|
||||||
return chroma;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getTone() {
|
|
||||||
return tone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int toInt() {
|
|
||||||
return argb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the hue of this color. Chroma may decrease because chroma has a different maximum for any
|
|
||||||
* given hue and tone.
|
|
||||||
*
|
|
||||||
* @param newHue 0 <= newHue < 360; invalid values are corrected.
|
|
||||||
*/
|
|
||||||
public void setHue(double newHue) {
|
|
||||||
setInternalState(HctSolver.solveToInt(newHue, chroma, tone));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the chroma of this color. Chroma may decrease because chroma has a different maximum for
|
|
||||||
* any given hue and tone.
|
|
||||||
*
|
|
||||||
* @param newChroma 0 <= newChroma < ?
|
|
||||||
*/
|
|
||||||
public void setChroma(double newChroma) {
|
|
||||||
setInternalState(HctSolver.solveToInt(hue, newChroma, tone));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the tone of this color. Chroma may decrease because chroma has a different maximum for any
|
|
||||||
* given hue and tone.
|
|
||||||
*
|
|
||||||
* @param newTone 0 <= newTone <= 100; invalid valids are corrected.
|
|
||||||
*/
|
|
||||||
public void setTone(double newTone) {
|
|
||||||
setInternalState(HctSolver.solveToInt(hue, chroma, newTone));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a color into different ViewingConditions.
|
|
||||||
*
|
|
||||||
* <p>Colors change appearance. They look different with lights on versus off, the same color, as
|
|
||||||
* in hex code, on white looks different when on black. This is called color relativity, most
|
|
||||||
* famously explicated by Josef Albers in Interaction of Color.
|
|
||||||
*
|
|
||||||
* <p>In color science, color appearance models can account for this and calculate the appearance
|
|
||||||
* of a color in different settings. HCT is based on CAM16, a color appearance model, and uses it
|
|
||||||
* to make these calculations.
|
|
||||||
*
|
|
||||||
* <p>See ViewingConditions.make for parameters affecting color appearance.
|
|
||||||
*/
|
|
||||||
public Hct inViewingConditions(ViewingConditions vc) {
|
|
||||||
// 1. Use CAM16 to find XYZ coordinates of color in specified VC.
|
|
||||||
Cam16 cam16 = Cam16.fromInt(toInt());
|
|
||||||
double[] viewedInVc = cam16.xyzInViewingConditions(vc, null);
|
|
||||||
|
|
||||||
// 2. Create CAM16 of those XYZ coordinates in default VC.
|
|
||||||
Cam16 recastInVc =
|
|
||||||
Cam16.fromXyzInViewingConditions(
|
|
||||||
viewedInVc[0], viewedInVc[1], viewedInVc[2], ViewingConditions.DEFAULT);
|
|
||||||
|
|
||||||
// 3. Create HCT from:
|
|
||||||
// - CAM16 using default VC with XYZ coordinates in specified VC.
|
|
||||||
// - L* converted from Y in XYZ coordinates in specified VC.
|
|
||||||
return Hct.from(
|
|
||||||
recastInVc.getHue(), recastInVc.getChroma(), ColorUtils.lstarFromY(viewedInVc[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInternalState(int argb) {
|
|
||||||
this.argb = argb;
|
|
||||||
Cam16 cam = Cam16.fromInt(argb);
|
|
||||||
hue = cam.getHue();
|
|
||||||
chroma = cam.getChroma();
|
|
||||||
this.tone = ColorUtils.lstarFromArgb(argb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
177
libs/material-color-utilities/src/main/java/hct/Hct.kt
Normal file
177
libs/material-color-utilities/src/main/java/hct/Hct.kt
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package hct
|
||||||
|
|
||||||
|
import utils.ColorUtils
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A color system built using CAM16 hue and chroma, and L* from L*a*b*.
|
||||||
|
*
|
||||||
|
* Using L* creates a link between the color system, contrast, and thus accessibility. Contrast
|
||||||
|
* ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can
|
||||||
|
* be calculated from Y.
|
||||||
|
*
|
||||||
|
* Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
|
||||||
|
*
|
||||||
|
* Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A difference
|
||||||
|
* of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50 guarantees a
|
||||||
|
* contrast ratio >= 4.5.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color
|
||||||
|
* measurement system that can also accurately render what colors will appear as in different
|
||||||
|
* lighting environments.
|
||||||
|
*/
|
||||||
|
class Hct private constructor(argb: Int) {
|
||||||
|
var hue = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
var chroma = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
var tone = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var argb = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
setInternalState(argb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toInt(): Int {
|
||||||
|
return argb
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the hue of this color. Chroma may decrease because chroma has a different maximum for any
|
||||||
|
* given hue and tone.
|
||||||
|
*
|
||||||
|
* @param newHue 0 <= newHue < 360; invalid values are corrected.
|
||||||
|
*/
|
||||||
|
fun setHue(newHue: Double) {
|
||||||
|
setInternalState(HctSolver.solveToInt(newHue, chroma, tone))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the chroma of this color. Chroma may decrease because chroma has a different maximum for
|
||||||
|
* any given hue and tone.
|
||||||
|
*
|
||||||
|
* @param newChroma 0 <= newChroma < ?
|
||||||
|
*/
|
||||||
|
fun setChroma(newChroma: Double) {
|
||||||
|
setInternalState(HctSolver.solveToInt(hue, newChroma, tone))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the tone of this color. Chroma may decrease because chroma has a different maximum for any
|
||||||
|
* given hue and tone.
|
||||||
|
*
|
||||||
|
* @param newTone 0 <= newTone <= 100; invalid valids are corrected.
|
||||||
|
*/
|
||||||
|
fun setTone(newTone: Double) {
|
||||||
|
setInternalState(HctSolver.solveToInt(hue, chroma, newTone))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "HCT(${hue.roundToInt()}, ${chroma.roundToInt()}, ${tone.roundToInt()})"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate a color into different ViewingConditions.
|
||||||
|
*
|
||||||
|
* Colors change appearance. They look different with lights on versus off, the same color, as in
|
||||||
|
* hex code, on white looks different when on black. This is called color relativity, most
|
||||||
|
* famously explicated by Josef Albers in Interaction of Color.
|
||||||
|
*
|
||||||
|
* In color science, color appearance models can account for this and calculate the appearance of
|
||||||
|
* a color in different settings. HCT is based on CAM16, a color appearance model, and uses it to
|
||||||
|
* make these calculations.
|
||||||
|
*
|
||||||
|
* See ViewingConditions.make for parameters affecting color appearance.
|
||||||
|
*/
|
||||||
|
fun inViewingConditions(vc: ViewingConditions): Hct {
|
||||||
|
// 1. Use CAM16 to find XYZ coordinates of color in specified VC.
|
||||||
|
val cam16 = Cam16.fromInt(toInt())
|
||||||
|
val viewedInVc = cam16.xyzInViewingConditions(vc, null)
|
||||||
|
|
||||||
|
// 2. Create CAM16 of those XYZ coordinates in default VC.
|
||||||
|
val recastInVc =
|
||||||
|
Cam16.fromXyzInViewingConditions(
|
||||||
|
viewedInVc[0],
|
||||||
|
viewedInVc[1],
|
||||||
|
viewedInVc[2],
|
||||||
|
ViewingConditions.DEFAULT,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. Create HCT from:
|
||||||
|
// - CAM16 using default VC with XYZ coordinates in specified VC.
|
||||||
|
// - L* converted from Y in XYZ coordinates in specified VC.
|
||||||
|
return from(recastInVc.hue, recastInVc.chroma, ColorUtils.lstarFromY(viewedInVc[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInternalState(argb: Int) {
|
||||||
|
this.argb = argb
|
||||||
|
val cam = Cam16.fromInt(argb)
|
||||||
|
hue = cam.hue
|
||||||
|
chroma = cam.chroma
|
||||||
|
tone = ColorUtils.lstarFromArgb(argb)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create an HCT color from hue, chroma, and tone.
|
||||||
|
*
|
||||||
|
* @param hue 0 <= hue < 360; invalid values are corrected.
|
||||||
|
* @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower than
|
||||||
|
* the requested chroma. Chroma has a different maximum for any given hue and tone.
|
||||||
|
* @param tone 0 <= tone <= 100; invalid values are corrected.
|
||||||
|
* @return HCT representation of a color in default viewing conditions.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun from(hue: Double, chroma: Double, tone: Double): Hct {
|
||||||
|
val argb = HctSolver.solveToInt(hue, chroma, tone)
|
||||||
|
return Hct(argb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an HCT color from a color.
|
||||||
|
*
|
||||||
|
* @param argb ARGB representation of a color.
|
||||||
|
* @return HCT representation of a color in default viewing conditions
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromInt(argb: Int): Hct {
|
||||||
|
return Hct(argb)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isBlue(hue: Double): Boolean {
|
||||||
|
return hue >= 250 && hue < 270
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isYellow(hue: Double): Boolean {
|
||||||
|
return hue >= 105 && hue < 125
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isCyan(hue: Double): Boolean {
|
||||||
|
return hue >= 170 && hue < 207
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,672 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This file is automatically generated. Do not modify it.
|
|
||||||
|
|
||||||
package hct;
|
|
||||||
|
|
||||||
import utils.ColorUtils;
|
|
||||||
import utils.MathUtils;
|
|
||||||
|
|
||||||
/** A class that solves the HCT equation. */
|
|
||||||
public class HctSolver {
|
|
||||||
private HctSolver() {}
|
|
||||||
|
|
||||||
static final double[][] SCALED_DISCOUNT_FROM_LINRGB =
|
|
||||||
new double[][] {
|
|
||||||
new double[] {
|
|
||||||
0.001200833568784504, 0.002389694492170889, 0.0002795742885861124,
|
|
||||||
},
|
|
||||||
new double[] {
|
|
||||||
0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398,
|
|
||||||
},
|
|
||||||
new double[] {
|
|
||||||
0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static final double[][] LINRGB_FROM_SCALED_DISCOUNT =
|
|
||||||
new double[][] {
|
|
||||||
new double[] {
|
|
||||||
1373.2198709594231, -1100.4251190754821, -7.278681089101213,
|
|
||||||
},
|
|
||||||
new double[] {
|
|
||||||
-271.815969077903, 559.6580465940733, -32.46047482791194,
|
|
||||||
},
|
|
||||||
new double[] {
|
|
||||||
1.9622899599665666, -57.173814538844006, 308.7233197812385,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static final double[] Y_FROM_LINRGB = new double[] {0.2126, 0.7152, 0.0722};
|
|
||||||
|
|
||||||
static final double[] CRITICAL_PLANES =
|
|
||||||
new double[] {
|
|
||||||
0.015176349177441876,
|
|
||||||
0.045529047532325624,
|
|
||||||
0.07588174588720938,
|
|
||||||
0.10623444424209313,
|
|
||||||
0.13658714259697685,
|
|
||||||
0.16693984095186062,
|
|
||||||
0.19729253930674434,
|
|
||||||
0.2276452376616281,
|
|
||||||
0.2579979360165119,
|
|
||||||
0.28835063437139563,
|
|
||||||
0.3188300904430532,
|
|
||||||
0.350925934958123,
|
|
||||||
0.3848314933096426,
|
|
||||||
0.42057480301049466,
|
|
||||||
0.458183274052838,
|
|
||||||
0.4976837250274023,
|
|
||||||
0.5391024159806381,
|
|
||||||
0.5824650784040898,
|
|
||||||
0.6277969426914107,
|
|
||||||
0.6751227633498623,
|
|
||||||
0.7244668422128921,
|
|
||||||
0.775853049866786,
|
|
||||||
0.829304845476233,
|
|
||||||
0.8848452951698498,
|
|
||||||
0.942497089126609,
|
|
||||||
1.0022825574869039,
|
|
||||||
1.0642236851973577,
|
|
||||||
1.1283421258858297,
|
|
||||||
1.1946592148522128,
|
|
||||||
1.2631959812511864,
|
|
||||||
1.3339731595349034,
|
|
||||||
1.407011200216447,
|
|
||||||
1.4823302800086415,
|
|
||||||
1.5599503113873272,
|
|
||||||
1.6398909516233677,
|
|
||||||
1.7221716113234105,
|
|
||||||
1.8068114625156377,
|
|
||||||
1.8938294463134073,
|
|
||||||
1.9832442801866852,
|
|
||||||
2.075074464868551,
|
|
||||||
2.1693382909216234,
|
|
||||||
2.2660538449872063,
|
|
||||||
2.36523901573795,
|
|
||||||
2.4669114995532007,
|
|
||||||
2.5710888059345764,
|
|
||||||
2.6777882626779785,
|
|
||||||
2.7870270208169257,
|
|
||||||
2.898822059350997,
|
|
||||||
3.0131901897720907,
|
|
||||||
3.1301480604002863,
|
|
||||||
3.2497121605402226,
|
|
||||||
3.3718988244681087,
|
|
||||||
3.4967242352587946,
|
|
||||||
3.624204428461639,
|
|
||||||
3.754355295633311,
|
|
||||||
3.887192587735158,
|
|
||||||
4.022731918402185,
|
|
||||||
4.160988767090289,
|
|
||||||
4.301978482107941,
|
|
||||||
4.445716283538092,
|
|
||||||
4.592217266055746,
|
|
||||||
4.741496401646282,
|
|
||||||
4.893568542229298,
|
|
||||||
5.048448422192488,
|
|
||||||
5.20615066083972,
|
|
||||||
5.3666897647573375,
|
|
||||||
5.5300801301023865,
|
|
||||||
5.696336044816294,
|
|
||||||
5.865471690767354,
|
|
||||||
6.037501145825082,
|
|
||||||
6.212438385869475,
|
|
||||||
6.390297286737924,
|
|
||||||
6.571091626112461,
|
|
||||||
6.7548350853498045,
|
|
||||||
6.941541251256611,
|
|
||||||
7.131223617812143,
|
|
||||||
7.323895587840543,
|
|
||||||
7.5195704746346665,
|
|
||||||
7.7182615035334345,
|
|
||||||
7.919981813454504,
|
|
||||||
8.124744458384042,
|
|
||||||
8.332562408825165,
|
|
||||||
8.543448553206703,
|
|
||||||
8.757415699253682,
|
|
||||||
8.974476575321063,
|
|
||||||
9.194643831691977,
|
|
||||||
9.417930041841839,
|
|
||||||
9.644347703669503,
|
|
||||||
9.873909240696694,
|
|
||||||
10.106627003236781,
|
|
||||||
10.342513269534024,
|
|
||||||
10.58158024687427,
|
|
||||||
10.8238400726681,
|
|
||||||
11.069304815507364,
|
|
||||||
11.317986476196008,
|
|
||||||
11.569896988756009,
|
|
||||||
11.825048221409341,
|
|
||||||
12.083451977536606,
|
|
||||||
12.345119996613247,
|
|
||||||
12.610063955123938,
|
|
||||||
12.878295467455942,
|
|
||||||
13.149826086772048,
|
|
||||||
13.42466730586372,
|
|
||||||
13.702830557985108,
|
|
||||||
13.984327217668513,
|
|
||||||
14.269168601521828,
|
|
||||||
14.55736596900856,
|
|
||||||
14.848930523210871,
|
|
||||||
15.143873411576273,
|
|
||||||
15.44220572664832,
|
|
||||||
15.743938506781891,
|
|
||||||
16.04908273684337,
|
|
||||||
16.35764934889634,
|
|
||||||
16.66964922287304,
|
|
||||||
16.985093187232053,
|
|
||||||
17.30399201960269,
|
|
||||||
17.62635644741625,
|
|
||||||
17.95219714852476,
|
|
||||||
18.281524751807332,
|
|
||||||
18.614349837764564,
|
|
||||||
18.95068293910138,
|
|
||||||
19.290534541298456,
|
|
||||||
19.633915083172692,
|
|
||||||
19.98083495742689,
|
|
||||||
20.331304511189067,
|
|
||||||
20.685334046541502,
|
|
||||||
21.042933821039977,
|
|
||||||
21.404114048223256,
|
|
||||||
21.76888489811322,
|
|
||||||
22.137256497705877,
|
|
||||||
22.50923893145328,
|
|
||||||
22.884842241736916,
|
|
||||||
23.264076429332462,
|
|
||||||
23.6469514538663,
|
|
||||||
24.033477234264016,
|
|
||||||
24.42366364919083,
|
|
||||||
24.817520537484558,
|
|
||||||
25.21505769858089,
|
|
||||||
25.61628489293138,
|
|
||||||
26.021211842414342,
|
|
||||||
26.429848230738664,
|
|
||||||
26.842203703840827,
|
|
||||||
27.258287870275353,
|
|
||||||
27.678110301598522,
|
|
||||||
28.10168053274597,
|
|
||||||
28.529008062403893,
|
|
||||||
28.96010235337422,
|
|
||||||
29.39497283293396,
|
|
||||||
29.83362889318845,
|
|
||||||
30.276079891419332,
|
|
||||||
30.722335150426627,
|
|
||||||
31.172403958865512,
|
|
||||||
31.62629557157785,
|
|
||||||
32.08401920991837,
|
|
||||||
32.54558406207592,
|
|
||||||
33.010999283389665,
|
|
||||||
33.4802739966603,
|
|
||||||
33.953417292456834,
|
|
||||||
34.430438229418264,
|
|
||||||
34.911345834551085,
|
|
||||||
35.39614910352207,
|
|
||||||
35.88485700094671,
|
|
||||||
36.37747846067349,
|
|
||||||
36.87402238606382,
|
|
||||||
37.37449765026789,
|
|
||||||
37.87891309649659,
|
|
||||||
38.38727753828926,
|
|
||||||
38.89959975977785,
|
|
||||||
39.41588851594697,
|
|
||||||
39.93615253289054,
|
|
||||||
40.460400508064545,
|
|
||||||
40.98864111053629,
|
|
||||||
41.520882981230194,
|
|
||||||
42.05713473317016,
|
|
||||||
42.597404951718396,
|
|
||||||
43.141702194811224,
|
|
||||||
43.6900349931913,
|
|
||||||
44.24241185063697,
|
|
||||||
44.798841244188324,
|
|
||||||
45.35933162437017,
|
|
||||||
45.92389141541209,
|
|
||||||
46.49252901546552,
|
|
||||||
47.065252796817916,
|
|
||||||
47.64207110610409,
|
|
||||||
48.22299226451468,
|
|
||||||
48.808024568002054,
|
|
||||||
49.3971762874833,
|
|
||||||
49.9904556690408,
|
|
||||||
50.587870934119984,
|
|
||||||
51.189430279724725,
|
|
||||||
51.79514187861014,
|
|
||||||
52.40501387947288,
|
|
||||||
53.0190544071392,
|
|
||||||
53.637271562750364,
|
|
||||||
54.259673423945976,
|
|
||||||
54.88626804504493,
|
|
||||||
55.517063457223934,
|
|
||||||
56.15206766869424,
|
|
||||||
56.79128866487574,
|
|
||||||
57.43473440856916,
|
|
||||||
58.08241284012621,
|
|
||||||
58.734331877617365,
|
|
||||||
59.39049941699807,
|
|
||||||
60.05092333227251,
|
|
||||||
60.715611475655585,
|
|
||||||
61.38457167773311,
|
|
||||||
62.057811747619894,
|
|
||||||
62.7353394731159,
|
|
||||||
63.417162620860914,
|
|
||||||
64.10328893648692,
|
|
||||||
64.79372614476921,
|
|
||||||
65.48848194977529,
|
|
||||||
66.18756403501224,
|
|
||||||
66.89098006357258,
|
|
||||||
67.59873767827808,
|
|
||||||
68.31084450182222,
|
|
||||||
69.02730813691093,
|
|
||||||
69.74813616640164,
|
|
||||||
70.47333615344107,
|
|
||||||
71.20291564160104,
|
|
||||||
71.93688215501312,
|
|
||||||
72.67524319850172,
|
|
||||||
73.41800625771542,
|
|
||||||
74.16517879925733,
|
|
||||||
74.9167682708136,
|
|
||||||
75.67278210128072,
|
|
||||||
76.43322770089146,
|
|
||||||
77.1981124613393,
|
|
||||||
77.96744375590167,
|
|
||||||
78.74122893956174,
|
|
||||||
79.51947534912904,
|
|
||||||
80.30219030335869,
|
|
||||||
81.08938110306934,
|
|
||||||
81.88105503125999,
|
|
||||||
82.67721935322541,
|
|
||||||
83.4778813166706,
|
|
||||||
84.28304815182372,
|
|
||||||
85.09272707154808,
|
|
||||||
85.90692527145302,
|
|
||||||
86.72564993000343,
|
|
||||||
87.54890820862819,
|
|
||||||
88.3767072518277,
|
|
||||||
89.2090541872801,
|
|
||||||
90.04595612594655,
|
|
||||||
90.88742016217518,
|
|
||||||
91.73345337380438,
|
|
||||||
92.58406282226491,
|
|
||||||
93.43925555268066,
|
|
||||||
94.29903859396902,
|
|
||||||
95.16341895893969,
|
|
||||||
96.03240364439274,
|
|
||||||
96.9059996312159,
|
|
||||||
97.78421388448044,
|
|
||||||
98.6670533535366,
|
|
||||||
99.55452497210776,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes a small enough angle in radians.
|
|
||||||
*
|
|
||||||
* @param angle An angle in radians; must not deviate too much from 0.
|
|
||||||
* @return A coterminal angle between 0 and 2pi.
|
|
||||||
*/
|
|
||||||
static double sanitizeRadians(double angle) {
|
|
||||||
return (angle + Math.PI * 8) % (Math.PI * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delinearizes an RGB component, returning a floating-point number.
|
|
||||||
*
|
|
||||||
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
|
|
||||||
* @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
|
|
||||||
*/
|
|
||||||
static double trueDelinearized(double rgbComponent) {
|
|
||||||
double normalized = rgbComponent / 100.0;
|
|
||||||
double delinearized = 0.0;
|
|
||||||
if (normalized <= 0.0031308) {
|
|
||||||
delinearized = normalized * 12.92;
|
|
||||||
} else {
|
|
||||||
delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
|
|
||||||
}
|
|
||||||
return delinearized * 255.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static double chromaticAdaptation(double component) {
|
|
||||||
double af = Math.pow(Math.abs(component), 0.42);
|
|
||||||
return MathUtils.signum(component) * 400.0 * af / (af + 27.13);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the hue of a linear RGB color in CAM16.
|
|
||||||
*
|
|
||||||
* @param linrgb The linear RGB coordinates of a color.
|
|
||||||
* @return The hue of the color in CAM16, in radians.
|
|
||||||
*/
|
|
||||||
static double hueOf(double[] linrgb) {
|
|
||||||
double[] scaledDiscount = MathUtils.matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB);
|
|
||||||
double rA = chromaticAdaptation(scaledDiscount[0]);
|
|
||||||
double gA = chromaticAdaptation(scaledDiscount[1]);
|
|
||||||
double bA = chromaticAdaptation(scaledDiscount[2]);
|
|
||||||
// redness-greenness
|
|
||||||
double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
|
|
||||||
// yellowness-blueness
|
|
||||||
double b = (rA + gA - 2.0 * bA) / 9.0;
|
|
||||||
return Math.atan2(b, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean areInCyclicOrder(double a, double b, double c) {
|
|
||||||
double deltaAB = sanitizeRadians(b - a);
|
|
||||||
double deltaAC = sanitizeRadians(c - a);
|
|
||||||
return deltaAB < deltaAC;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Solves the lerp equation.
|
|
||||||
*
|
|
||||||
* @param source The starting number.
|
|
||||||
* @param mid The number in the middle.
|
|
||||||
* @param target The ending number.
|
|
||||||
* @return A number t such that lerp(source, target, t) = mid.
|
|
||||||
*/
|
|
||||||
static double intercept(double source, double mid, double target) {
|
|
||||||
return (mid - source) / (target - source);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double[] lerpPoint(double[] source, double t, double[] target) {
|
|
||||||
return new double[] {
|
|
||||||
source[0] + (target[0] - source[0]) * t,
|
|
||||||
source[1] + (target[1] - source[1]) * t,
|
|
||||||
source[2] + (target[2] - source[2]) * t,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intersects a segment with a plane.
|
|
||||||
*
|
|
||||||
* @param source The coordinates of point A.
|
|
||||||
* @param coordinate The R-, G-, or B-coordinate of the plane.
|
|
||||||
* @param target The coordinates of point B.
|
|
||||||
* @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
|
|
||||||
* @return The intersection point of the segment AB with the plane R=coordinate, G=coordinate, or
|
|
||||||
* B=coordinate
|
|
||||||
*/
|
|
||||||
static double[] setCoordinate(double[] source, double coordinate, double[] target, int axis) {
|
|
||||||
double t = intercept(source[axis], coordinate, target[axis]);
|
|
||||||
return lerpPoint(source, t, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isBounded(double x) {
|
|
||||||
return 0.0 <= x && x <= 100.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the nth possible vertex of the polygonal intersection.
|
|
||||||
*
|
|
||||||
* @param y The Y value of the plane.
|
|
||||||
* @param n The zero-based index of the point. 0 <= n <= 11.
|
|
||||||
* @return The nth possible vertex of the polygonal intersection of the y plane and the RGB cube,
|
|
||||||
* in linear RGB coordinates, if it exists. If this possible vertex lies outside of the cube,
|
|
||||||
* [-1.0, -1.0, -1.0] is returned.
|
|
||||||
*/
|
|
||||||
static double[] nthVertex(double y, int n) {
|
|
||||||
double kR = Y_FROM_LINRGB[0];
|
|
||||||
double kG = Y_FROM_LINRGB[1];
|
|
||||||
double kB = Y_FROM_LINRGB[2];
|
|
||||||
double coordA = n % 4 <= 1 ? 0.0 : 100.0;
|
|
||||||
double coordB = n % 2 == 0 ? 0.0 : 100.0;
|
|
||||||
if (n < 4) {
|
|
||||||
double g = coordA;
|
|
||||||
double b = coordB;
|
|
||||||
double r = (y - g * kG - b * kB) / kR;
|
|
||||||
if (isBounded(r)) {
|
|
||||||
return new double[] {r, g, b};
|
|
||||||
} else {
|
|
||||||
return new double[] {-1.0, -1.0, -1.0};
|
|
||||||
}
|
|
||||||
} else if (n < 8) {
|
|
||||||
double b = coordA;
|
|
||||||
double r = coordB;
|
|
||||||
double g = (y - r * kR - b * kB) / kG;
|
|
||||||
if (isBounded(g)) {
|
|
||||||
return new double[] {r, g, b};
|
|
||||||
} else {
|
|
||||||
return new double[] {-1.0, -1.0, -1.0};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
double r = coordA;
|
|
||||||
double g = coordB;
|
|
||||||
double b = (y - r * kR - g * kG) / kB;
|
|
||||||
if (isBounded(b)) {
|
|
||||||
return new double[] {r, g, b};
|
|
||||||
} else {
|
|
||||||
return new double[] {-1.0, -1.0, -1.0};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the segment containing the desired color.
|
|
||||||
*
|
|
||||||
* @param y The Y value of the color.
|
|
||||||
* @param targetHue The hue of the color.
|
|
||||||
* @return A list of two sets of linear RGB coordinates, each corresponding to an endpoint of the
|
|
||||||
* segment containing the desired color.
|
|
||||||
*/
|
|
||||||
static double[][] bisectToSegment(double y, double targetHue) {
|
|
||||||
double[] left = new double[] {-1.0, -1.0, -1.0};
|
|
||||||
double[] right = left;
|
|
||||||
double leftHue = 0.0;
|
|
||||||
double rightHue = 0.0;
|
|
||||||
boolean initialized = false;
|
|
||||||
boolean uncut = true;
|
|
||||||
for (int n = 0; n < 12; n++) {
|
|
||||||
double[] mid = nthVertex(y, n);
|
|
||||||
if (mid[0] < 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double midHue = hueOf(mid);
|
|
||||||
if (!initialized) {
|
|
||||||
left = mid;
|
|
||||||
right = mid;
|
|
||||||
leftHue = midHue;
|
|
||||||
rightHue = midHue;
|
|
||||||
initialized = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
|
|
||||||
uncut = false;
|
|
||||||
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
|
||||||
right = mid;
|
|
||||||
rightHue = midHue;
|
|
||||||
} else {
|
|
||||||
left = mid;
|
|
||||||
leftHue = midHue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new double[][] {left, right};
|
|
||||||
}
|
|
||||||
|
|
||||||
static double[] midpoint(double[] a, double[] b) {
|
|
||||||
return new double[] {
|
|
||||||
(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static int criticalPlaneBelow(double x) {
|
|
||||||
return (int) Math.floor(x - 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int criticalPlaneAbove(double x) {
|
|
||||||
return (int) Math.ceil(x - 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a color with the given Y and hue on the boundary of the cube.
|
|
||||||
*
|
|
||||||
* @param y The Y value of the color.
|
|
||||||
* @param targetHue The hue of the color.
|
|
||||||
* @return The desired color, in linear RGB coordinates.
|
|
||||||
*/
|
|
||||||
static double[] bisectToLimit(double y, double targetHue) {
|
|
||||||
double[][] segment = bisectToSegment(y, targetHue);
|
|
||||||
double[] left = segment[0];
|
|
||||||
double leftHue = hueOf(left);
|
|
||||||
double[] right = segment[1];
|
|
||||||
for (int axis = 0; axis < 3; axis++) {
|
|
||||||
if (left[axis] != right[axis]) {
|
|
||||||
int lPlane = -1;
|
|
||||||
int rPlane = 255;
|
|
||||||
if (left[axis] < right[axis]) {
|
|
||||||
lPlane = criticalPlaneBelow(trueDelinearized(left[axis]));
|
|
||||||
rPlane = criticalPlaneAbove(trueDelinearized(right[axis]));
|
|
||||||
} else {
|
|
||||||
lPlane = criticalPlaneAbove(trueDelinearized(left[axis]));
|
|
||||||
rPlane = criticalPlaneBelow(trueDelinearized(right[axis]));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 8; i++) {
|
|
||||||
if (Math.abs(rPlane - lPlane) <= 1) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
int mPlane = (int) Math.floor((lPlane + rPlane) / 2.0);
|
|
||||||
double midPlaneCoordinate = CRITICAL_PLANES[mPlane];
|
|
||||||
double[] mid = setCoordinate(left, midPlaneCoordinate, right, axis);
|
|
||||||
double midHue = hueOf(mid);
|
|
||||||
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
|
||||||
right = mid;
|
|
||||||
rPlane = mPlane;
|
|
||||||
} else {
|
|
||||||
left = mid;
|
|
||||||
leftHue = midHue;
|
|
||||||
lPlane = mPlane;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return midpoint(left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double inverseChromaticAdaptation(double adapted) {
|
|
||||||
double adaptedAbs = Math.abs(adapted);
|
|
||||||
double base = Math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
|
|
||||||
return MathUtils.signum(adapted) * Math.pow(base, 1.0 / 0.42);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a color with the given hue, chroma, and Y.
|
|
||||||
*
|
|
||||||
* @param hueRadians The desired hue in radians.
|
|
||||||
* @param chroma The desired chroma.
|
|
||||||
* @param y The desired Y.
|
|
||||||
* @return The desired color as a hexadecimal integer, if found; 0 otherwise.
|
|
||||||
*/
|
|
||||||
static int findResultByJ(double hueRadians, double chroma, double y) {
|
|
||||||
// Initial estimate of j.
|
|
||||||
double j = Math.sqrt(y) * 11.0;
|
|
||||||
// ===========================================================
|
|
||||||
// Operations inlined from Cam16 to avoid repeated calculation
|
|
||||||
// ===========================================================
|
|
||||||
ViewingConditions viewingConditions = ViewingConditions.DEFAULT;
|
|
||||||
double tInnerCoeff = 1 / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73);
|
|
||||||
double eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8);
|
|
||||||
double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
|
|
||||||
double hSin = Math.sin(hueRadians);
|
|
||||||
double hCos = Math.cos(hueRadians);
|
|
||||||
for (int iterationRound = 0; iterationRound < 5; iterationRound++) {
|
|
||||||
// ===========================================================
|
|
||||||
// Operations inlined from Cam16 to avoid repeated calculation
|
|
||||||
// ===========================================================
|
|
||||||
double jNormalized = j / 100.0;
|
|
||||||
double alpha = chroma == 0.0 || j == 0.0 ? 0.0 : chroma / Math.sqrt(jNormalized);
|
|
||||||
double t = Math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
|
|
||||||
double ac =
|
|
||||||
viewingConditions.getAw()
|
|
||||||
* Math.pow(jNormalized, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
|
|
||||||
double p2 = ac / viewingConditions.getNbb();
|
|
||||||
double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
|
|
||||||
double a = gamma * hCos;
|
|
||||||
double b = gamma * hSin;
|
|
||||||
double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
|
|
||||||
double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
|
|
||||||
double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
|
|
||||||
double rCScaled = inverseChromaticAdaptation(rA);
|
|
||||||
double gCScaled = inverseChromaticAdaptation(gA);
|
|
||||||
double bCScaled = inverseChromaticAdaptation(bA);
|
|
||||||
double[] linrgb =
|
|
||||||
MathUtils.matrixMultiply(
|
|
||||||
new double[] {rCScaled, gCScaled, bCScaled}, LINRGB_FROM_SCALED_DISCOUNT);
|
|
||||||
// ===========================================================
|
|
||||||
// Operations inlined from Cam16 to avoid repeated calculation
|
|
||||||
// ===========================================================
|
|
||||||
if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
double kR = Y_FROM_LINRGB[0];
|
|
||||||
double kG = Y_FROM_LINRGB[1];
|
|
||||||
double kB = Y_FROM_LINRGB[2];
|
|
||||||
double fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2];
|
|
||||||
if (fnj <= 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (iterationRound == 4 || Math.abs(fnj - y) < 0.002) {
|
|
||||||
if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return ColorUtils.argbFromLinrgb(linrgb);
|
|
||||||
}
|
|
||||||
// Iterates with Newton method,
|
|
||||||
// Using 2 * fn(j) / j as the approximation of fn'(j)
|
|
||||||
j = j - (fnj - y) * j / (2 * fnj);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
|
|
||||||
*
|
|
||||||
* @param hueDegrees The desired hue, in degrees.
|
|
||||||
* @param chroma The desired chroma.
|
|
||||||
* @param lstar The desired L*.
|
|
||||||
* @return A hexadecimal representing the sRGB color. The color has sufficiently close hue,
|
|
||||||
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
|
|
||||||
* sufficiently close, and chroma will be maximized.
|
|
||||||
*/
|
|
||||||
public static int solveToInt(double hueDegrees, double chroma, double lstar) {
|
|
||||||
if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
|
|
||||||
return ColorUtils.argbFromLstar(lstar);
|
|
||||||
}
|
|
||||||
hueDegrees = MathUtils.sanitizeDegreesDouble(hueDegrees);
|
|
||||||
double hueRadians = hueDegrees / 180 * Math.PI;
|
|
||||||
double y = ColorUtils.yFromLstar(lstar);
|
|
||||||
int exactAnswer = findResultByJ(hueRadians, chroma, y);
|
|
||||||
if (exactAnswer != 0) {
|
|
||||||
return exactAnswer;
|
|
||||||
}
|
|
||||||
double[] linrgb = bisectToLimit(y, hueRadians);
|
|
||||||
return ColorUtils.argbFromLinrgb(linrgb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
|
|
||||||
*
|
|
||||||
* @param hueDegrees The desired hue, in degrees.
|
|
||||||
* @param chroma The desired chroma.
|
|
||||||
* @param lstar The desired L*.
|
|
||||||
* @return An CAM16 object representing the sRGB color. The color has sufficiently close hue,
|
|
||||||
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
|
|
||||||
* sufficiently close, and chroma will be maximized.
|
|
||||||
*/
|
|
||||||
public static Cam16 solveToCam(double hueDegrees, double chroma, double lstar) {
|
|
||||||
return Cam16.fromInt(solveToInt(hueDegrees, chroma, lstar));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
670
libs/material-color-utilities/src/main/java/hct/HctSolver.kt
Normal file
670
libs/material-color-utilities/src/main/java/hct/HctSolver.kt
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
// This file is automatically generated. Do not modify it.
|
||||||
|
package hct
|
||||||
|
|
||||||
|
import utils.ColorUtils
|
||||||
|
import utils.MathUtils
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sign
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/** A class that solves the HCT equation. */
|
||||||
|
object HctSolver {
|
||||||
|
private val SCALED_DISCOUNT_FROM_LINRGB =
|
||||||
|
arrayOf(
|
||||||
|
doubleArrayOf(0.001200833568784504, 0.002389694492170889, 0.0002795742885861124),
|
||||||
|
doubleArrayOf(0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398),
|
||||||
|
doubleArrayOf(0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076),
|
||||||
|
)
|
||||||
|
private val LINRGB_FROM_SCALED_DISCOUNT =
|
||||||
|
arrayOf(
|
||||||
|
doubleArrayOf(1373.2198709594231, -1100.4251190754821, -7.278681089101213),
|
||||||
|
doubleArrayOf(-271.815969077903, 559.6580465940733, -32.46047482791194),
|
||||||
|
doubleArrayOf(1.9622899599665666, -57.173814538844006, 308.7233197812385),
|
||||||
|
)
|
||||||
|
private val Y_FROM_LINRGB = doubleArrayOf(0.2126, 0.7152, 0.0722)
|
||||||
|
private val CRITICAL_PLANES =
|
||||||
|
doubleArrayOf(
|
||||||
|
0.015176349177441876,
|
||||||
|
0.045529047532325624,
|
||||||
|
0.07588174588720938,
|
||||||
|
0.10623444424209313,
|
||||||
|
0.13658714259697685,
|
||||||
|
0.16693984095186062,
|
||||||
|
0.19729253930674434,
|
||||||
|
0.2276452376616281,
|
||||||
|
0.2579979360165119,
|
||||||
|
0.28835063437139563,
|
||||||
|
0.3188300904430532,
|
||||||
|
0.350925934958123,
|
||||||
|
0.3848314933096426,
|
||||||
|
0.42057480301049466,
|
||||||
|
0.458183274052838,
|
||||||
|
0.4976837250274023,
|
||||||
|
0.5391024159806381,
|
||||||
|
0.5824650784040898,
|
||||||
|
0.6277969426914107,
|
||||||
|
0.6751227633498623,
|
||||||
|
0.7244668422128921,
|
||||||
|
0.775853049866786,
|
||||||
|
0.829304845476233,
|
||||||
|
0.8848452951698498,
|
||||||
|
0.942497089126609,
|
||||||
|
1.0022825574869039,
|
||||||
|
1.0642236851973577,
|
||||||
|
1.1283421258858297,
|
||||||
|
1.1946592148522128,
|
||||||
|
1.2631959812511864,
|
||||||
|
1.3339731595349034,
|
||||||
|
1.407011200216447,
|
||||||
|
1.4823302800086415,
|
||||||
|
1.5599503113873272,
|
||||||
|
1.6398909516233677,
|
||||||
|
1.7221716113234105,
|
||||||
|
1.8068114625156377,
|
||||||
|
1.8938294463134073,
|
||||||
|
1.9832442801866852,
|
||||||
|
2.075074464868551,
|
||||||
|
2.1693382909216234,
|
||||||
|
2.2660538449872063,
|
||||||
|
2.36523901573795,
|
||||||
|
2.4669114995532007,
|
||||||
|
2.5710888059345764,
|
||||||
|
2.6777882626779785,
|
||||||
|
2.7870270208169257,
|
||||||
|
2.898822059350997,
|
||||||
|
3.0131901897720907,
|
||||||
|
3.1301480604002863,
|
||||||
|
3.2497121605402226,
|
||||||
|
3.3718988244681087,
|
||||||
|
3.4967242352587946,
|
||||||
|
3.624204428461639,
|
||||||
|
3.754355295633311,
|
||||||
|
3.887192587735158,
|
||||||
|
4.022731918402185,
|
||||||
|
4.160988767090289,
|
||||||
|
4.301978482107941,
|
||||||
|
4.445716283538092,
|
||||||
|
4.592217266055746,
|
||||||
|
4.741496401646282,
|
||||||
|
4.893568542229298,
|
||||||
|
5.048448422192488,
|
||||||
|
5.20615066083972,
|
||||||
|
5.3666897647573375,
|
||||||
|
5.5300801301023865,
|
||||||
|
5.696336044816294,
|
||||||
|
5.865471690767354,
|
||||||
|
6.037501145825082,
|
||||||
|
6.212438385869475,
|
||||||
|
6.390297286737924,
|
||||||
|
6.571091626112461,
|
||||||
|
6.7548350853498045,
|
||||||
|
6.941541251256611,
|
||||||
|
7.131223617812143,
|
||||||
|
7.323895587840543,
|
||||||
|
7.5195704746346665,
|
||||||
|
7.7182615035334345,
|
||||||
|
7.919981813454504,
|
||||||
|
8.124744458384042,
|
||||||
|
8.332562408825165,
|
||||||
|
8.543448553206703,
|
||||||
|
8.757415699253682,
|
||||||
|
8.974476575321063,
|
||||||
|
9.194643831691977,
|
||||||
|
9.417930041841839,
|
||||||
|
9.644347703669503,
|
||||||
|
9.873909240696694,
|
||||||
|
10.106627003236781,
|
||||||
|
10.342513269534024,
|
||||||
|
10.58158024687427,
|
||||||
|
10.8238400726681,
|
||||||
|
11.069304815507364,
|
||||||
|
11.317986476196008,
|
||||||
|
11.569896988756009,
|
||||||
|
11.825048221409341,
|
||||||
|
12.083451977536606,
|
||||||
|
12.345119996613247,
|
||||||
|
12.610063955123938,
|
||||||
|
12.878295467455942,
|
||||||
|
13.149826086772048,
|
||||||
|
13.42466730586372,
|
||||||
|
13.702830557985108,
|
||||||
|
13.984327217668513,
|
||||||
|
14.269168601521828,
|
||||||
|
14.55736596900856,
|
||||||
|
14.848930523210871,
|
||||||
|
15.143873411576273,
|
||||||
|
15.44220572664832,
|
||||||
|
15.743938506781891,
|
||||||
|
16.04908273684337,
|
||||||
|
16.35764934889634,
|
||||||
|
16.66964922287304,
|
||||||
|
16.985093187232053,
|
||||||
|
17.30399201960269,
|
||||||
|
17.62635644741625,
|
||||||
|
17.95219714852476,
|
||||||
|
18.281524751807332,
|
||||||
|
18.614349837764564,
|
||||||
|
18.95068293910138,
|
||||||
|
19.290534541298456,
|
||||||
|
19.633915083172692,
|
||||||
|
19.98083495742689,
|
||||||
|
20.331304511189067,
|
||||||
|
20.685334046541502,
|
||||||
|
21.042933821039977,
|
||||||
|
21.404114048223256,
|
||||||
|
21.76888489811322,
|
||||||
|
22.137256497705877,
|
||||||
|
22.50923893145328,
|
||||||
|
22.884842241736916,
|
||||||
|
23.264076429332462,
|
||||||
|
23.6469514538663,
|
||||||
|
24.033477234264016,
|
||||||
|
24.42366364919083,
|
||||||
|
24.817520537484558,
|
||||||
|
25.21505769858089,
|
||||||
|
25.61628489293138,
|
||||||
|
26.021211842414342,
|
||||||
|
26.429848230738664,
|
||||||
|
26.842203703840827,
|
||||||
|
27.258287870275353,
|
||||||
|
27.678110301598522,
|
||||||
|
28.10168053274597,
|
||||||
|
28.529008062403893,
|
||||||
|
28.96010235337422,
|
||||||
|
29.39497283293396,
|
||||||
|
29.83362889318845,
|
||||||
|
30.276079891419332,
|
||||||
|
30.722335150426627,
|
||||||
|
31.172403958865512,
|
||||||
|
31.62629557157785,
|
||||||
|
32.08401920991837,
|
||||||
|
32.54558406207592,
|
||||||
|
33.010999283389665,
|
||||||
|
33.4802739966603,
|
||||||
|
33.953417292456834,
|
||||||
|
34.430438229418264,
|
||||||
|
34.911345834551085,
|
||||||
|
35.39614910352207,
|
||||||
|
35.88485700094671,
|
||||||
|
36.37747846067349,
|
||||||
|
36.87402238606382,
|
||||||
|
37.37449765026789,
|
||||||
|
37.87891309649659,
|
||||||
|
38.38727753828926,
|
||||||
|
38.89959975977785,
|
||||||
|
39.41588851594697,
|
||||||
|
39.93615253289054,
|
||||||
|
40.460400508064545,
|
||||||
|
40.98864111053629,
|
||||||
|
41.520882981230194,
|
||||||
|
42.05713473317016,
|
||||||
|
42.597404951718396,
|
||||||
|
43.141702194811224,
|
||||||
|
43.6900349931913,
|
||||||
|
44.24241185063697,
|
||||||
|
44.798841244188324,
|
||||||
|
45.35933162437017,
|
||||||
|
45.92389141541209,
|
||||||
|
46.49252901546552,
|
||||||
|
47.065252796817916,
|
||||||
|
47.64207110610409,
|
||||||
|
48.22299226451468,
|
||||||
|
48.808024568002054,
|
||||||
|
49.3971762874833,
|
||||||
|
49.9904556690408,
|
||||||
|
50.587870934119984,
|
||||||
|
51.189430279724725,
|
||||||
|
51.79514187861014,
|
||||||
|
52.40501387947288,
|
||||||
|
53.0190544071392,
|
||||||
|
53.637271562750364,
|
||||||
|
54.259673423945976,
|
||||||
|
54.88626804504493,
|
||||||
|
55.517063457223934,
|
||||||
|
56.15206766869424,
|
||||||
|
56.79128866487574,
|
||||||
|
57.43473440856916,
|
||||||
|
58.08241284012621,
|
||||||
|
58.734331877617365,
|
||||||
|
59.39049941699807,
|
||||||
|
60.05092333227251,
|
||||||
|
60.715611475655585,
|
||||||
|
61.38457167773311,
|
||||||
|
62.057811747619894,
|
||||||
|
62.7353394731159,
|
||||||
|
63.417162620860914,
|
||||||
|
64.10328893648692,
|
||||||
|
64.79372614476921,
|
||||||
|
65.48848194977529,
|
||||||
|
66.18756403501224,
|
||||||
|
66.89098006357258,
|
||||||
|
67.59873767827808,
|
||||||
|
68.31084450182222,
|
||||||
|
69.02730813691093,
|
||||||
|
69.74813616640164,
|
||||||
|
70.47333615344107,
|
||||||
|
71.20291564160104,
|
||||||
|
71.93688215501312,
|
||||||
|
72.67524319850172,
|
||||||
|
73.41800625771542,
|
||||||
|
74.16517879925733,
|
||||||
|
74.9167682708136,
|
||||||
|
75.67278210128072,
|
||||||
|
76.43322770089146,
|
||||||
|
77.1981124613393,
|
||||||
|
77.96744375590167,
|
||||||
|
78.74122893956174,
|
||||||
|
79.51947534912904,
|
||||||
|
80.30219030335869,
|
||||||
|
81.08938110306934,
|
||||||
|
81.88105503125999,
|
||||||
|
82.67721935322541,
|
||||||
|
83.4778813166706,
|
||||||
|
84.28304815182372,
|
||||||
|
85.09272707154808,
|
||||||
|
85.90692527145302,
|
||||||
|
86.72564993000343,
|
||||||
|
87.54890820862819,
|
||||||
|
88.3767072518277,
|
||||||
|
89.2090541872801,
|
||||||
|
90.04595612594655,
|
||||||
|
90.88742016217518,
|
||||||
|
91.73345337380438,
|
||||||
|
92.58406282226491,
|
||||||
|
93.43925555268066,
|
||||||
|
94.29903859396902,
|
||||||
|
95.16341895893969,
|
||||||
|
96.03240364439274,
|
||||||
|
96.9059996312159,
|
||||||
|
97.78421388448044,
|
||||||
|
98.6670533535366,
|
||||||
|
99.55452497210776,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a small enough angle in radians.
|
||||||
|
*
|
||||||
|
* @param angle An angle in radians; must not deviate too much from 0.
|
||||||
|
* @return A coterminal angle between 0 and 2pi.
|
||||||
|
*/
|
||||||
|
internal fun sanitizeRadians(angle: Double): Double {
|
||||||
|
return (angle + PI * 8) % (PI * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delinearizes an RGB component, returning a floating-point number.
|
||||||
|
*
|
||||||
|
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
|
||||||
|
* @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
|
||||||
|
*/
|
||||||
|
internal fun trueDelinearized(rgbComponent: Double): Double {
|
||||||
|
val normalized = rgbComponent / 100.0
|
||||||
|
val delinearized: Double =
|
||||||
|
if (normalized <= 0.0031308) {
|
||||||
|
normalized * 12.92
|
||||||
|
} else {
|
||||||
|
1.055 * normalized.pow(1.0 / 2.4) - 0.055
|
||||||
|
}
|
||||||
|
return delinearized * 255.0
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun chromaticAdaptation(component: Double): Double {
|
||||||
|
val af = abs(component).pow(0.42)
|
||||||
|
return sign(component) * 400.0 * af / (af + 27.13)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hue of a linear RGB color in CAM16.
|
||||||
|
*
|
||||||
|
* @param linrgb The linear RGB coordinates of a color.
|
||||||
|
* @return The hue of the color in CAM16, in radians.
|
||||||
|
*/
|
||||||
|
internal fun hueOf(linrgb: DoubleArray): Double {
|
||||||
|
val scaledDiscount = MathUtils.matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB)
|
||||||
|
val rA = chromaticAdaptation(scaledDiscount[0])
|
||||||
|
val gA = chromaticAdaptation(scaledDiscount[1])
|
||||||
|
val bA = chromaticAdaptation(scaledDiscount[2])
|
||||||
|
// redness-greenness
|
||||||
|
val a = (11.0 * rA + -12.0 * gA + bA) / 11.0
|
||||||
|
// yellowness-blueness
|
||||||
|
val b = (rA + gA - 2.0 * bA) / 9.0
|
||||||
|
return atan2(b, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun areInCyclicOrder(a: Double, b: Double, c: Double): Boolean {
|
||||||
|
val deltaAB = sanitizeRadians(b - a)
|
||||||
|
val deltaAC = sanitizeRadians(c - a)
|
||||||
|
return deltaAB < deltaAC
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solves the lerp equation.
|
||||||
|
*
|
||||||
|
* @param source The starting number.
|
||||||
|
* @param mid The number in the middle.
|
||||||
|
* @param target The ending number.
|
||||||
|
* @return A number t such that lerp(source, target, t) = mid.
|
||||||
|
*/
|
||||||
|
internal fun intercept(source: Double, mid: Double, target: Double): Double {
|
||||||
|
return (mid - source) / (target - source)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun lerpPoint(source: DoubleArray, t: Double, target: DoubleArray): DoubleArray {
|
||||||
|
return doubleArrayOf(
|
||||||
|
source[0] + (target[0] - source[0]) * t,
|
||||||
|
source[1] + (target[1] - source[1]) * t,
|
||||||
|
source[2] + (target[2] - source[2]) * t,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intersects a segment with a plane.
|
||||||
|
*
|
||||||
|
* @param source The coordinates of point A.
|
||||||
|
* @param coordinate The R-, G-, or B-coordinate of the plane.
|
||||||
|
* @param target The coordinates of point B.
|
||||||
|
* @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
|
||||||
|
* @return The intersection point of the segment AB with the plane R=coordinate, G=coordinate, or
|
||||||
|
* B=coordinate
|
||||||
|
*/
|
||||||
|
internal fun setCoordinate(
|
||||||
|
source: DoubleArray,
|
||||||
|
coordinate: Double,
|
||||||
|
target: DoubleArray,
|
||||||
|
axis: Int,
|
||||||
|
): DoubleArray {
|
||||||
|
val t = intercept(source[axis], coordinate, target[axis])
|
||||||
|
return lerpPoint(source, t, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun isBounded(x: Double): Boolean {
|
||||||
|
return 0.0 <= x && x <= 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the nth possible vertex of the polygonal intersection.
|
||||||
|
*
|
||||||
|
* @param y The Y value of the plane.
|
||||||
|
* @param n The zero-based index of the point. 0 <= n <= 11.
|
||||||
|
* @return The nth possible vertex of the polygonal intersection of the y plane and the RGB cube,
|
||||||
|
* in linear RGB coordinates, if it exists. If this possible vertex lies outside of the cube,
|
||||||
|
* null is returned.
|
||||||
|
*/
|
||||||
|
internal fun nthVertex(y: Double, n: Int): DoubleArray? {
|
||||||
|
val kR = Y_FROM_LINRGB[0]
|
||||||
|
val kG = Y_FROM_LINRGB[1]
|
||||||
|
val kB = Y_FROM_LINRGB[2]
|
||||||
|
val coordA = if (n % 4 <= 1) 0.0 else 100.0
|
||||||
|
val coordB = if (n % 2 == 0) 0.0 else 100.0
|
||||||
|
return when {
|
||||||
|
n < 4 -> {
|
||||||
|
val g = coordA
|
||||||
|
val b = coordB
|
||||||
|
val r = (y - g * kG - b * kB) / kR
|
||||||
|
if (isBounded(r)) {
|
||||||
|
doubleArrayOf(r, g, b)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n < 8 -> {
|
||||||
|
val b = coordA
|
||||||
|
val r = coordB
|
||||||
|
val g = (y - r * kR - b * kB) / kG
|
||||||
|
if (isBounded(g)) {
|
||||||
|
doubleArrayOf(r, g, b)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val r = coordA
|
||||||
|
val g = coordB
|
||||||
|
val b = (y - r * kR - g * kG) / kB
|
||||||
|
if (isBounded(b)) {
|
||||||
|
doubleArrayOf(r, g, b)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the segment containing the desired color.
|
||||||
|
*
|
||||||
|
* @param y The Y value of the color.
|
||||||
|
* @param targetHue The hue of the color.
|
||||||
|
* @return A list of two sets of linear RGB coordinates, each corresponding to an endpoint of the
|
||||||
|
* segment containing the desired color.
|
||||||
|
*/
|
||||||
|
internal fun bisectToSegment(y: Double, targetHue: Double): Array<DoubleArray> {
|
||||||
|
var left: DoubleArray? = null
|
||||||
|
var right: DoubleArray? = null
|
||||||
|
var leftHue = 0.0
|
||||||
|
var rightHue = 0.0
|
||||||
|
var initialized = false
|
||||||
|
var uncut = true
|
||||||
|
for (n in 0..11) {
|
||||||
|
val mid = nthVertex(y, n)
|
||||||
|
if (mid != null) {
|
||||||
|
val midHue = hueOf(mid)
|
||||||
|
if (!initialized) {
|
||||||
|
left = mid
|
||||||
|
right = mid
|
||||||
|
leftHue = midHue
|
||||||
|
rightHue = midHue
|
||||||
|
initialized = true
|
||||||
|
} else if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
|
||||||
|
uncut = false
|
||||||
|
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
||||||
|
right = mid
|
||||||
|
rightHue = midHue
|
||||||
|
} else {
|
||||||
|
left = mid
|
||||||
|
leftHue = midHue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arrayOf(left!!, right!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun midpoint(a: DoubleArray, b: DoubleArray): DoubleArray {
|
||||||
|
return doubleArrayOf((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun criticalPlaneBelow(x: Double): Int {
|
||||||
|
return floor(x - 0.5).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun criticalPlaneAbove(x: Double): Int {
|
||||||
|
return ceil(x - 0.5).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a color with the given Y and hue on the boundary of the cube.
|
||||||
|
*
|
||||||
|
* @param y The Y value of the color.
|
||||||
|
* @param targetHue The hue of the color.
|
||||||
|
* @return The desired color, in linear RGB coordinates.
|
||||||
|
*/
|
||||||
|
internal fun bisectToLimit(y: Double, targetHue: Double): DoubleArray {
|
||||||
|
val segment = bisectToSegment(y, targetHue)
|
||||||
|
var left = segment[0]
|
||||||
|
var leftHue = hueOf(left)
|
||||||
|
var right = segment[1]
|
||||||
|
for (axis in 0..2) {
|
||||||
|
if (left[axis] != right[axis]) {
|
||||||
|
var lPlane =
|
||||||
|
if (left[axis] < right[axis]) {
|
||||||
|
criticalPlaneBelow(trueDelinearized(left[axis]))
|
||||||
|
} else {
|
||||||
|
criticalPlaneAbove(trueDelinearized(left[axis]))
|
||||||
|
}
|
||||||
|
var rPlane =
|
||||||
|
if (left[axis] < right[axis]) {
|
||||||
|
criticalPlaneAbove(trueDelinearized(right[axis]))
|
||||||
|
} else {
|
||||||
|
criticalPlaneBelow(trueDelinearized(right[axis]))
|
||||||
|
}
|
||||||
|
for (i in 0..7) {
|
||||||
|
if (abs((rPlane - lPlane).toDouble()) <= 1) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
val mPlane = floor((lPlane + rPlane) / 2.0).toInt()
|
||||||
|
val midPlaneCoordinate = CRITICAL_PLANES[mPlane]
|
||||||
|
val mid = setCoordinate(left, midPlaneCoordinate, right, axis)
|
||||||
|
val midHue = hueOf(mid)
|
||||||
|
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
||||||
|
right = mid
|
||||||
|
rPlane = mPlane
|
||||||
|
} else {
|
||||||
|
left = mid
|
||||||
|
leftHue = midHue
|
||||||
|
lPlane = mPlane
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return midpoint(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun inverseChromaticAdaptation(adapted: Double): Double {
|
||||||
|
val adaptedAbs = abs(adapted)
|
||||||
|
val base = Math.max(0.0, 27.13 * adaptedAbs / (400.0 - adaptedAbs))
|
||||||
|
return sign(adapted) * base.pow(1.0 / 0.42)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a color with the given hue, chroma, and Y.
|
||||||
|
*
|
||||||
|
* @param hueRadians The desired hue in radians.
|
||||||
|
* @param chroma The desired chroma.
|
||||||
|
* @param y The desired Y.
|
||||||
|
* @return The desired color as a hexadecimal integer, if found; 0 otherwise.
|
||||||
|
*/
|
||||||
|
internal fun findResultByJ(hueRadians: Double, chroma: Double, y: Double): Int {
|
||||||
|
// Initial estimate of j.
|
||||||
|
var j = sqrt(y) * 11.0
|
||||||
|
// ===========================================================
|
||||||
|
// Operations inlined from Cam16 to avoid repeated calculation
|
||||||
|
// ===========================================================
|
||||||
|
val viewingConditions = ViewingConditions.DEFAULT
|
||||||
|
val tInnerCoeff = 1 / (1.64 - 0.29.pow(viewingConditions.n)).pow(0.73)
|
||||||
|
val eHue = 0.25 * (cos(hueRadians + 2.0) + 3.8)
|
||||||
|
val p1 = eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb
|
||||||
|
val hSin = sin(hueRadians)
|
||||||
|
val hCos = cos(hueRadians)
|
||||||
|
for (iterationRound in 0..4) {
|
||||||
|
// ===========================================================
|
||||||
|
// Operations inlined from Cam16 to avoid repeated calculation
|
||||||
|
// ===========================================================
|
||||||
|
val jNormalized = j / 100.0
|
||||||
|
val alpha = if (chroma == 0.0 || j == 0.0) 0.0 else chroma / sqrt(jNormalized)
|
||||||
|
val t = (alpha * tInnerCoeff).pow(1.0 / 0.9)
|
||||||
|
val ac =
|
||||||
|
viewingConditions.aw * jNormalized.pow(1.0 / viewingConditions.c / viewingConditions.z)
|
||||||
|
val p2 = ac / viewingConditions.nbb
|
||||||
|
val gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin)
|
||||||
|
val a = gamma * hCos
|
||||||
|
val b = gamma * hSin
|
||||||
|
val rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0
|
||||||
|
val gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0
|
||||||
|
val bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0
|
||||||
|
val rCScaled = inverseChromaticAdaptation(rA)
|
||||||
|
val gCScaled = inverseChromaticAdaptation(gA)
|
||||||
|
val bCScaled = inverseChromaticAdaptation(bA)
|
||||||
|
val linrgb =
|
||||||
|
MathUtils.matrixMultiply(
|
||||||
|
doubleArrayOf(rCScaled, gCScaled, bCScaled),
|
||||||
|
LINRGB_FROM_SCALED_DISCOUNT,
|
||||||
|
)
|
||||||
|
// ===========================================================
|
||||||
|
// Operations inlined from Cam16 to avoid repeated calculation
|
||||||
|
// ===========================================================
|
||||||
|
if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
val kR = Y_FROM_LINRGB[0]
|
||||||
|
val kG = Y_FROM_LINRGB[1]
|
||||||
|
val kB = Y_FROM_LINRGB[2]
|
||||||
|
val fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2]
|
||||||
|
if (fnj <= 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (iterationRound == 4 || abs(fnj - y) < 0.002) {
|
||||||
|
return if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
ColorUtils.argbFromLinrgb(linrgb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Iterates with Newton method,
|
||||||
|
// Using 2 * fn(j) / j as the approximation of fn'(j)
|
||||||
|
j -= (fnj - y) * j / (2 * fnj)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
|
||||||
|
*
|
||||||
|
* @param hueDegrees The desired hue, in degrees.
|
||||||
|
* @param chroma The desired chroma.
|
||||||
|
* @param lstar The desired L*.
|
||||||
|
* @return A hexadecimal representing the sRGB color. The color has sufficiently close hue,
|
||||||
|
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
|
||||||
|
* sufficiently close, and chroma will be maximized.
|
||||||
|
*/
|
||||||
|
fun solveToInt(hueDegrees: Double, chroma: Double, lstar: Double): Int {
|
||||||
|
if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
|
||||||
|
return ColorUtils.argbFromLstar(lstar)
|
||||||
|
}
|
||||||
|
val hueRadians = MathUtils.sanitizeDegreesDouble(hueDegrees) / 180 * PI
|
||||||
|
val y = ColorUtils.yFromLstar(lstar)
|
||||||
|
val exactAnswer = findResultByJ(hueRadians, chroma, y)
|
||||||
|
if (exactAnswer != 0) {
|
||||||
|
return exactAnswer
|
||||||
|
}
|
||||||
|
val linrgb = bisectToLimit(y, hueRadians)
|
||||||
|
return ColorUtils.argbFromLinrgb(linrgb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
|
||||||
|
*
|
||||||
|
* @param hueDegrees The desired hue, in degrees.
|
||||||
|
* @param chroma The desired chroma.
|
||||||
|
* @param lstar The desired L*.
|
||||||
|
* @return A CAM16 object representing the sRGB color. The color has sufficiently close hue,
|
||||||
|
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
|
||||||
|
* sufficiently close, and chroma will be maximized.
|
||||||
|
*/
|
||||||
|
fun solveToCam(hueDegrees: Double, chroma: Double, lstar: Double): Cam16 {
|
||||||
|
return Cam16.fromInt(solveToInt(hueDegrees, chroma, lstar))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package hct;
|
|
||||||
|
|
||||||
import utils.ColorUtils;
|
|
||||||
import utils.MathUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In traditional color spaces, a color can be identified solely by the observer's measurement of
|
|
||||||
* the color. Color appearance models such as CAM16 also use information about the environment where
|
|
||||||
* the color was observed, known as the viewing conditions.
|
|
||||||
*
|
|
||||||
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
|
|
||||||
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
|
|
||||||
*
|
|
||||||
* <p>This class caches intermediate values of the CAM16 conversion process that depend only on
|
|
||||||
* viewing conditions, enabling speed ups.
|
|
||||||
*/
|
|
||||||
public final class ViewingConditions {
|
|
||||||
/** sRGB-like viewing conditions. */
|
|
||||||
public static final ViewingConditions DEFAULT =
|
|
||||||
ViewingConditions.defaultWithBackgroundLstar(50.0);
|
|
||||||
|
|
||||||
private final double aw;
|
|
||||||
private final double nbb;
|
|
||||||
private final double ncb;
|
|
||||||
private final double c;
|
|
||||||
private final double nc;
|
|
||||||
private final double n;
|
|
||||||
private final double[] rgbD;
|
|
||||||
private final double fl;
|
|
||||||
private final double flRoot;
|
|
||||||
private final double z;
|
|
||||||
|
|
||||||
public double getAw() {
|
|
||||||
return aw;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getN() {
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getNbb() {
|
|
||||||
return nbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
double getNcb() {
|
|
||||||
return ncb;
|
|
||||||
}
|
|
||||||
|
|
||||||
double getC() {
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
double getNc() {
|
|
||||||
return nc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double[] getRgbD() {
|
|
||||||
return rgbD;
|
|
||||||
}
|
|
||||||
|
|
||||||
double getFl() {
|
|
||||||
return fl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getFlRoot() {
|
|
||||||
return flRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
double getZ() {
|
|
||||||
return z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create ViewingConditions from a simple, physically relevant, set of parameters.
|
|
||||||
*
|
|
||||||
* @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day
|
|
||||||
* afternoon
|
|
||||||
* @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in
|
|
||||||
* the room where the color is viewed. Can be calculated from lux by multiplying lux by
|
|
||||||
* 0.0586. default = 11.72, or 200 lux.
|
|
||||||
* @param backgroundLstar The lightness of the area surrounding the color. measured by L* in
|
|
||||||
* L*a*b*. default = 50.0
|
|
||||||
* @param surround A general description of the lighting surrounding the color. 0 is pitch dark,
|
|
||||||
* like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at
|
|
||||||
* night. 2.0 means there is no difference between the lighting on the color and around it.
|
|
||||||
* default = 2.0
|
|
||||||
* @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
|
|
||||||
* such as knowing an apple is still red in green light. default = false, the eye does not
|
|
||||||
* perform this process on self-luminous objects like displays.
|
|
||||||
*/
|
|
||||||
public static ViewingConditions make(
|
|
||||||
double[] whitePoint,
|
|
||||||
double adaptingLuminance,
|
|
||||||
double backgroundLstar,
|
|
||||||
double surround,
|
|
||||||
boolean discountingIlluminant) {
|
|
||||||
// A background of pure black is non-physical and leads to infinities that represent the idea
|
|
||||||
// that any color viewed in pure black can't be seen.
|
|
||||||
backgroundLstar = Math.max(0.1, backgroundLstar);
|
|
||||||
// Transform white point XYZ to 'cone'/'rgb' responses
|
|
||||||
double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
|
|
||||||
double[] xyz = whitePoint;
|
|
||||||
double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
|
|
||||||
double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
|
|
||||||
double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
|
|
||||||
double f = 0.8 + (surround / 10.0);
|
|
||||||
double c =
|
|
||||||
(f >= 0.9)
|
|
||||||
? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0))
|
|
||||||
: MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
|
|
||||||
double d =
|
|
||||||
discountingIlluminant
|
|
||||||
? 1.0
|
|
||||||
: f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0)));
|
|
||||||
d = MathUtils.clampDouble(0.0, 1.0, d);
|
|
||||||
double nc = f;
|
|
||||||
double[] rgbD =
|
|
||||||
new double[] {
|
|
||||||
d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d, d * (100.0 / bW) + 1.0 - d
|
|
||||||
};
|
|
||||||
double k = 1.0 / (5.0 * adaptingLuminance + 1.0);
|
|
||||||
double k4 = k * k * k * k;
|
|
||||||
double k4F = 1.0 - k4;
|
|
||||||
double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(5.0 * adaptingLuminance));
|
|
||||||
double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
|
|
||||||
double z = 1.48 + Math.sqrt(n);
|
|
||||||
double nbb = 0.725 / Math.pow(n, 0.2);
|
|
||||||
double ncb = nbb;
|
|
||||||
double[] rgbAFactors =
|
|
||||||
new double[] {
|
|
||||||
Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
|
|
||||||
Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
|
|
||||||
Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
|
|
||||||
};
|
|
||||||
|
|
||||||
double[] rgbA =
|
|
||||||
new double[] {
|
|
||||||
(400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
|
|
||||||
(400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
|
|
||||||
(400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13)
|
|
||||||
};
|
|
||||||
|
|
||||||
double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb;
|
|
||||||
return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create sRGB-like viewing conditions with a custom background lstar.
|
|
||||||
*
|
|
||||||
* <p>Default viewing conditions have a lstar of 50, midgray.
|
|
||||||
*/
|
|
||||||
public static ViewingConditions defaultWithBackgroundLstar(double lstar) {
|
|
||||||
return ViewingConditions.make(
|
|
||||||
ColorUtils.whitePointD65(),
|
|
||||||
(200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
|
|
||||||
lstar,
|
|
||||||
2.0,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand
|
|
||||||
* for technical color science terminology, this class would not benefit from documenting them
|
|
||||||
* individually. A brief overview is available in the CAM16 specification, and a complete overview
|
|
||||||
* requires a color science textbook, such as Fairchild's Color Appearance Models.
|
|
||||||
*/
|
|
||||||
private ViewingConditions(
|
|
||||||
double n,
|
|
||||||
double aw,
|
|
||||||
double nbb,
|
|
||||||
double ncb,
|
|
||||||
double c,
|
|
||||||
double nc,
|
|
||||||
double[] rgbD,
|
|
||||||
double fl,
|
|
||||||
double flRoot,
|
|
||||||
double z) {
|
|
||||||
this.n = n;
|
|
||||||
this.aw = aw;
|
|
||||||
this.nbb = nbb;
|
|
||||||
this.ncb = ncb;
|
|
||||||
this.c = c;
|
|
||||||
this.nc = nc;
|
|
||||||
this.rgbD = rgbD;
|
|
||||||
this.fl = fl;
|
|
||||||
this.flRoot = flRoot;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package hct
|
||||||
|
|
||||||
|
import utils.ColorUtils
|
||||||
|
import utils.MathUtils
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cbrt
|
||||||
|
import kotlin.math.exp
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In traditional color spaces, a color can be identified solely by the observer's measurement of
|
||||||
|
* the color. Color appearance models such as CAM16 also use information about the environment where
|
||||||
|
* the color was observed, known as the viewing conditions.
|
||||||
|
*
|
||||||
|
* For example, white under the traditional assumption of a midday sun white point is accurately
|
||||||
|
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
|
||||||
|
*
|
||||||
|
* This class caches intermediate values of the CAM16 conversion process that depend only on viewing
|
||||||
|
* conditions, enabling speed ups.
|
||||||
|
*/
|
||||||
|
@ConsistentCopyVisibility
|
||||||
|
data class ViewingConditions
|
||||||
|
/**
|
||||||
|
* Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand for
|
||||||
|
* technical color science terminology, this class would not benefit from documenting them
|
||||||
|
* individually. A brief overview is available in the CAM16 specification, and a complete overview
|
||||||
|
* requires a color science textbook, such as Fairchild's Color Appearance Models.
|
||||||
|
*/
|
||||||
|
private constructor(
|
||||||
|
val n: Double,
|
||||||
|
val aw: Double,
|
||||||
|
val nbb: Double,
|
||||||
|
internal val ncb: Double,
|
||||||
|
internal val c: Double,
|
||||||
|
internal val nc: Double,
|
||||||
|
val rgbD: DoubleArray,
|
||||||
|
internal val fl: Double,
|
||||||
|
val flRoot: Double,
|
||||||
|
internal val z: Double,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** sRGB-like viewing conditions. */
|
||||||
|
val DEFAULT = defaultWithBackgroundLstar(50.0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ViewingConditions from a simple, physically relevant, set of parameters.
|
||||||
|
*
|
||||||
|
* @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day
|
||||||
|
* afternoon
|
||||||
|
* @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in
|
||||||
|
* the room where the color is viewed. Can be calculated from lux by multiplying lux by
|
||||||
|
* 0.0586. default = 11.72, or 200 lux.
|
||||||
|
* @param backgroundLstar The lightness of the area surrounding the color. measured by L* in
|
||||||
|
* L*a*b*. default = 50.0
|
||||||
|
* @param surround A general description of the lighting surrounding the color. 0 is pitch dark,
|
||||||
|
* like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at
|
||||||
|
* night. 2.0 means there is no difference between the lighting on the color and around it.
|
||||||
|
* default = 2.0
|
||||||
|
* @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
|
||||||
|
* such as knowing an apple is still red in green light. default = false, the eye does not
|
||||||
|
* perform this process on self-luminous objects like displays.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun make(
|
||||||
|
whitePoint: DoubleArray,
|
||||||
|
adaptingLuminance: Double,
|
||||||
|
backgroundLstar: Double,
|
||||||
|
surround: Double,
|
||||||
|
discountingIlluminant: Boolean,
|
||||||
|
): ViewingConditions {
|
||||||
|
// A background of pure black is non-physical and leads to infinities that represent the idea
|
||||||
|
// that any color viewed in pure black can't be seen.
|
||||||
|
val backgroundLstar = max(0.1, backgroundLstar)
|
||||||
|
// Transform white point XYZ to 'cone'/'rgb' responses
|
||||||
|
val matrix = Cam16.XYZ_TO_CAM16RGB
|
||||||
|
val xyz = whitePoint
|
||||||
|
val rW = xyz[0] * matrix[0][0] + xyz[1] * matrix[0][1] + xyz[2] * matrix[0][2]
|
||||||
|
val gW = xyz[0] * matrix[1][0] + xyz[1] * matrix[1][1] + xyz[2] * matrix[1][2]
|
||||||
|
val bW = xyz[0] * matrix[2][0] + xyz[1] * matrix[2][1] + xyz[2] * matrix[2][2]
|
||||||
|
val f = 0.8 + surround / 10.0
|
||||||
|
val c =
|
||||||
|
if (f >= 0.9) {
|
||||||
|
MathUtils.lerp(0.59, 0.69, (f - 0.9) * 10.0)
|
||||||
|
} else {
|
||||||
|
MathUtils.lerp(0.525, 0.59, (f - 0.8) * 10.0)
|
||||||
|
}
|
||||||
|
var d =
|
||||||
|
if (discountingIlluminant) {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
f * (1.0 - 1.0 / 3.6 * exp((-adaptingLuminance - 42.0) / 92.0))
|
||||||
|
}
|
||||||
|
d = d.coerceIn(0.0, 1.0)
|
||||||
|
val nc = f
|
||||||
|
val rgbD =
|
||||||
|
doubleArrayOf(
|
||||||
|
d * (100.0 / rW) + 1.0 - d,
|
||||||
|
d * (100.0 / gW) + 1.0 - d,
|
||||||
|
d * (100.0 / bW) + 1.0 - d,
|
||||||
|
)
|
||||||
|
val k = 1.0 / (5.0 * adaptingLuminance + 1.0)
|
||||||
|
val k4 = k * k * k * k
|
||||||
|
val k4F = 1.0 - k4
|
||||||
|
val fl = k4 * adaptingLuminance + 0.1 * k4F * k4F * cbrt(5.0 * adaptingLuminance)
|
||||||
|
val n = ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]
|
||||||
|
val z = 1.48 + sqrt(n)
|
||||||
|
val nbb = 0.725 / n.pow(0.2)
|
||||||
|
val ncb = nbb
|
||||||
|
val rgbAFactors =
|
||||||
|
doubleArrayOf(
|
||||||
|
(fl * rgbD[0] * rW / 100.0).pow(0.42),
|
||||||
|
(fl * rgbD[1] * gW / 100.0).pow(0.42),
|
||||||
|
(fl * rgbD[2] * bW / 100.0).pow(0.42),
|
||||||
|
)
|
||||||
|
val rgbA =
|
||||||
|
doubleArrayOf(
|
||||||
|
400.0 * rgbAFactors[0] / (rgbAFactors[0] + 27.13),
|
||||||
|
400.0 * rgbAFactors[1] / (rgbAFactors[1] + 27.13),
|
||||||
|
400.0 * rgbAFactors[2] / (rgbAFactors[2] + 27.13),
|
||||||
|
)
|
||||||
|
val aw = (2.0 * rgbA[0] + rgbA[1] + 0.05 * rgbA[2]) * nbb
|
||||||
|
return ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, fl.pow(0.25), z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create sRGB-like viewing conditions with a custom background lstar.
|
||||||
|
*
|
||||||
|
* Default viewing conditions have a lstar of 50, midgray.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun defaultWithBackgroundLstar(lstar: Double): ViewingConditions {
|
||||||
|
return make(
|
||||||
|
ColorUtils.whitePointD65(),
|
||||||
|
200.0 / PI * ColorUtils.yFromLstar(50.0) / 100f,
|
||||||
|
lstar,
|
||||||
|
2.0,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package palettes;
|
|
||||||
|
|
||||||
import static java.lang.Math.max;
|
|
||||||
import static java.lang.Math.min;
|
|
||||||
|
|
||||||
import hct.Hct;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
public final class CorePalette {
|
|
||||||
public TonalPalette a1;
|
|
||||||
public TonalPalette a2;
|
|
||||||
public TonalPalette a3;
|
|
||||||
public TonalPalette n1;
|
|
||||||
public TonalPalette n2;
|
|
||||||
public TonalPalette error;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create key tones from a color.
|
|
||||||
*
|
|
||||||
* @param argb ARGB representation of a color
|
|
||||||
*/
|
|
||||||
public static CorePalette of(int argb) {
|
|
||||||
return new CorePalette(argb, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create content key tones from a color.
|
|
||||||
*
|
|
||||||
* @param argb ARGB representation of a color
|
|
||||||
*/
|
|
||||||
public static CorePalette contentOf(int argb) {
|
|
||||||
return new CorePalette(argb, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CorePalette(TonalPalette a1, TonalPalette a2, TonalPalette a3, TonalPalette n1, TonalPalette n2, TonalPalette error) {
|
|
||||||
this.a1 = a1;
|
|
||||||
this.a2 = a2;
|
|
||||||
this.a3 = a3;
|
|
||||||
this.n1 = n1;
|
|
||||||
this.n2 = n2;
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CorePalette(int argb, boolean isContent) {
|
|
||||||
Hct hct = Hct.fromInt(argb);
|
|
||||||
double hue = hct.getHue();
|
|
||||||
double chroma = hct.getChroma();
|
|
||||||
if (isContent) {
|
|
||||||
this.a1 = TonalPalette.fromHueAndChroma(hue, chroma);
|
|
||||||
this.a2 = TonalPalette.fromHueAndChroma(hue, chroma / 3.);
|
|
||||||
this.a3 = TonalPalette.fromHueAndChroma(hue + 60., chroma / 2.);
|
|
||||||
this.n1 = TonalPalette.fromHueAndChroma(hue, min(chroma / 12., 4.));
|
|
||||||
this.n2 = TonalPalette.fromHueAndChroma(hue, min(chroma / 6., 8.));
|
|
||||||
} else {
|
|
||||||
this.a1 = TonalPalette.fromHueAndChroma(hue, max(48., chroma));
|
|
||||||
this.a2 = TonalPalette.fromHueAndChroma(hue, 16.);
|
|
||||||
this.a3 = TonalPalette.fromHueAndChroma(hue + 60., 24.);
|
|
||||||
this.n1 = TonalPalette.fromHueAndChroma(hue, 4.);
|
|
||||||
this.n2 = TonalPalette.fromHueAndChroma(hue, 8.);
|
|
||||||
}
|
|
||||||
this.error = TonalPalette.fromHueAndChroma(25, 84.);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package palettes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprises foundational palettes to build a color scheme.
|
||||||
|
*
|
||||||
|
* Generated from a source color, these palettes will then be part of a [DynamicScheme] together
|
||||||
|
* with appearance preferences.
|
||||||
|
*/
|
||||||
|
data class CorePalettes(
|
||||||
|
val primary: TonalPalette,
|
||||||
|
val secondary: TonalPalette,
|
||||||
|
val tertiary: TonalPalette,
|
||||||
|
val neutral: TonalPalette,
|
||||||
|
val neutralVariant: TonalPalette,
|
||||||
|
)
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package palettes;
|
|
||||||
|
|
||||||
import hct.Hct;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
|
|
||||||
*/
|
|
||||||
public final class TonalPalette {
|
|
||||||
Map<Integer, Integer> cache;
|
|
||||||
Hct keyColor;
|
|
||||||
double hue;
|
|
||||||
double chroma;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create tones using the HCT hue and chroma from a color.
|
|
||||||
*
|
|
||||||
* @param argb ARGB representation of a color
|
|
||||||
* @return Tones matching that color's hue and chroma.
|
|
||||||
*/
|
|
||||||
public static TonalPalette fromInt(int argb) {
|
|
||||||
return fromHct(Hct.fromInt(argb));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create tones using a HCT color.
|
|
||||||
*
|
|
||||||
* @param hct HCT representation of a color.
|
|
||||||
* @return Tones matching that color's hue and chroma.
|
|
||||||
*/
|
|
||||||
public static TonalPalette fromHct(Hct hct) {
|
|
||||||
return new TonalPalette(hct.getHue(), hct.getChroma(), hct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create tones from a defined HCT hue and chroma.
|
|
||||||
*
|
|
||||||
* @param hue HCT hue
|
|
||||||
* @param chroma HCT chroma
|
|
||||||
* @return Tones matching hue and chroma.
|
|
||||||
*/
|
|
||||||
public static TonalPalette fromHueAndChroma(double hue, double chroma) {
|
|
||||||
return new TonalPalette(hue, chroma, createKeyColor(hue, chroma));
|
|
||||||
}
|
|
||||||
|
|
||||||
private TonalPalette(double hue, double chroma, Hct keyColor) {
|
|
||||||
cache = new HashMap<>();
|
|
||||||
this.hue = hue;
|
|
||||||
this.chroma = chroma;
|
|
||||||
this.keyColor = keyColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The key color is the first tone, starting from T50, matching the given hue and chroma. */
|
|
||||||
private static Hct createKeyColor(double hue, double chroma) {
|
|
||||||
double startTone = 50.0;
|
|
||||||
Hct smallestDeltaHct = Hct.from(hue, chroma, startTone);
|
|
||||||
double smallestDelta = Math.abs(smallestDeltaHct.getChroma() - chroma);
|
|
||||||
// Starting from T50, check T+/-delta to see if they match the requested
|
|
||||||
// chroma.
|
|
||||||
//
|
|
||||||
// Starts from T50 because T50 has the most chroma available, on
|
|
||||||
// average. Thus it is most likely to have a direct answer and minimize
|
|
||||||
// iteration.
|
|
||||||
for (double delta = 1.0; delta < 50.0; delta += 1.0) {
|
|
||||||
// Termination condition rounding instead of minimizing delta to avoid
|
|
||||||
// case where requested chroma is 16.51, and the closest chroma is 16.49.
|
|
||||||
// Error is minimized, but when rounded and displayed, requested chroma
|
|
||||||
// is 17, key color's chroma is 16.
|
|
||||||
if (Math.round(chroma) == Math.round(smallestDeltaHct.getChroma())) {
|
|
||||||
return smallestDeltaHct;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Hct hctAdd = Hct.from(hue, chroma, startTone + delta);
|
|
||||||
final double hctAddDelta = Math.abs(hctAdd.getChroma() - chroma);
|
|
||||||
if (hctAddDelta < smallestDelta) {
|
|
||||||
smallestDelta = hctAddDelta;
|
|
||||||
smallestDeltaHct = hctAdd;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Hct hctSubtract = Hct.from(hue, chroma, startTone - delta);
|
|
||||||
final double hctSubtractDelta = Math.abs(hctSubtract.getChroma() - chroma);
|
|
||||||
if (hctSubtractDelta < smallestDelta) {
|
|
||||||
smallestDelta = hctSubtractDelta;
|
|
||||||
smallestDeltaHct = hctSubtract;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return smallestDeltaHct;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
|
|
||||||
*
|
|
||||||
* @param tone HCT tone, measured from 0 to 100.
|
|
||||||
* @return ARGB representation of a color with that tone.
|
|
||||||
*/
|
|
||||||
// AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923)
|
|
||||||
@SuppressWarnings("ComputeIfAbsentUseValue")
|
|
||||||
public int tone(int tone) {
|
|
||||||
Integer color = cache.get(tone);
|
|
||||||
if (color == null) {
|
|
||||||
color = Hct.from(this.hue, this.chroma, tone).toInt();
|
|
||||||
cache.put(tone, color);
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
|
|
||||||
public Hct getHct(double tone) {
|
|
||||||
return Hct.from(this.hue, this.chroma, tone);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
|
|
||||||
public double getChroma() {
|
|
||||||
return this.chroma;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
|
|
||||||
public double getHue() {
|
|
||||||
return this.hue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The key color is the first tone, starting from T50, that matches the palette's chroma. */
|
|
||||||
public Hct getKeyColor() {
|
|
||||||
return this.keyColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package palettes
|
||||||
|
|
||||||
|
import hct.Hct
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
|
||||||
|
*
|
||||||
|
* TonalPalette is intended for use in a single thread due to its stateful caching.
|
||||||
|
*/
|
||||||
|
class TonalPalette
|
||||||
|
private constructor(
|
||||||
|
/** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
|
||||||
|
val hue: Double,
|
||||||
|
/** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
|
||||||
|
val chroma: Double,
|
||||||
|
/** The key color is the first tone, starting from T50, that matches the palette's chroma. */
|
||||||
|
val keyColor: Hct,
|
||||||
|
) {
|
||||||
|
var cache: MutableMap<Int, Int> = mutableMapOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
|
||||||
|
*
|
||||||
|
* @param tone HCT tone, measured from 0 to 100.
|
||||||
|
* @return ARGB representation of a color with that tone.
|
||||||
|
*/
|
||||||
|
fun tone(tone: Int): Int {
|
||||||
|
var color = cache[tone]
|
||||||
|
if (color == null) {
|
||||||
|
color =
|
||||||
|
if (tone == 99 && Hct.isYellow(hue)) {
|
||||||
|
averageArgb(tone(98), tone(100))
|
||||||
|
} else {
|
||||||
|
Hct.from(hue, chroma, tone.toDouble()).toInt()
|
||||||
|
}
|
||||||
|
cache[tone] = color
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
|
||||||
|
fun getHct(tone: Double): Hct {
|
||||||
|
return Hct.from(hue, chroma, tone)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun averageArgb(argb1: Int, argb2: Int): Int {
|
||||||
|
val red1 = argb1 ushr 16 and 0xff
|
||||||
|
val green1 = argb1 ushr 8 and 0xff
|
||||||
|
val blue1 = argb1 and 0xff
|
||||||
|
val red2 = argb2 ushr 16 and 0xff
|
||||||
|
val green2 = argb2 ushr 8 and 0xff
|
||||||
|
val blue2 = argb2 and 0xff
|
||||||
|
val red = ((red1 + red2) / 2f).roundToInt()
|
||||||
|
val green = ((green1 + green2) / 2f).roundToInt()
|
||||||
|
val blue = ((blue1 + blue2) / 2f).roundToInt()
|
||||||
|
return 255 shl 24 or (red and 255) shl 16 or (green and 255) shl 8 or (blue and 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Key color is a color that represents the hue and chroma of a tonal palette. */
|
||||||
|
private class KeyColor(private val hue: Double, private val requestedChroma: Double) {
|
||||||
|
// Cache that maps tone to max chroma to avoid duplicated HCT calculation.
|
||||||
|
private val chromaCache: MutableMap<Int, Double> = mutableMapOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a key color from a [hue] and a [chroma]. The key color is the first tone, starting
|
||||||
|
* from T50, matching the given hue and chroma.
|
||||||
|
*
|
||||||
|
* @return Key color [Hct]
|
||||||
|
*/
|
||||||
|
fun create(): Hct {
|
||||||
|
// Pivot around T50 because T50 has the most chroma available, on
|
||||||
|
// average. Thus it is most likely to have a direct answer.
|
||||||
|
val pivotTone = 50
|
||||||
|
val toneStepSize = 1
|
||||||
|
// Epsilon to accept values slightly higher than the requested chroma.
|
||||||
|
val epsilon = 0.01
|
||||||
|
|
||||||
|
// Binary search to find the tone that can provide a chroma that is closest
|
||||||
|
// to the requested chroma.
|
||||||
|
var lowerTone = 0
|
||||||
|
var upperTone = 100
|
||||||
|
while (lowerTone < upperTone) {
|
||||||
|
val midTone = (lowerTone + upperTone) / 2
|
||||||
|
val isAscending = maxChroma(midTone) < maxChroma(midTone + toneStepSize)
|
||||||
|
val sufficientChroma = maxChroma(midTone) >= requestedChroma - epsilon
|
||||||
|
if (sufficientChroma) {
|
||||||
|
// Either range [lowerTone, midTone] or [midTone, upperTone] has
|
||||||
|
// the answer, so search in the range that is closer the pivot tone.
|
||||||
|
if (abs((lowerTone - pivotTone).toDouble()) < abs((upperTone - pivotTone).toDouble())) {
|
||||||
|
upperTone = midTone
|
||||||
|
} else {
|
||||||
|
if (lowerTone == midTone) {
|
||||||
|
return Hct.from(hue, requestedChroma, lowerTone.toDouble())
|
||||||
|
}
|
||||||
|
lowerTone = midTone
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// As there is no sufficient chroma in the midTone, follow the direction to the chroma
|
||||||
|
// peak.
|
||||||
|
if (isAscending) {
|
||||||
|
lowerTone = midTone + toneStepSize
|
||||||
|
} else {
|
||||||
|
// Keep midTone for potential chroma peak.
|
||||||
|
upperTone = midTone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Hct.from(hue, requestedChroma, lowerTone.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the maximum chroma for a given tone
|
||||||
|
private fun maxChroma(tone: Int): Double {
|
||||||
|
return chromaCache.getOrPut(tone) { Hct.from(hue, MAX_CHROMA_VALUE, tone.toDouble()).chroma }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_CHROMA_VALUE = 200.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create tones using the HCT hue and chroma from a color.
|
||||||
|
*
|
||||||
|
* @param argb ARGB representation of a color
|
||||||
|
* @return Tones matching that color's hue and chroma.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromInt(argb: Int): TonalPalette {
|
||||||
|
return fromHct(Hct.fromInt(argb))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create tones using a HCT color.
|
||||||
|
*
|
||||||
|
* @param hct HCT representation of a color.
|
||||||
|
* @return Tones matching that color's hue and chroma.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromHct(hct: Hct): TonalPalette {
|
||||||
|
return TonalPalette(hct.hue, hct.chroma, hct)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create tones from a defined HCT hue and chroma.
|
||||||
|
*
|
||||||
|
* @param hue HCT hue
|
||||||
|
* @param chroma HCT chroma
|
||||||
|
* @return Tones matching hue and chroma.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun fromHueAndChroma(hue: Double, chroma: Double): TonalPalette {
|
||||||
|
val keyColor = KeyColor(hue, chroma).create()
|
||||||
|
return TonalPalette(hue, chroma, keyColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
/** An interface to allow use of different color spaces by quantizers. */
|
||||||
|
interface PointProvider {
|
||||||
|
/** The four components in the color space of an sRGB color. */
|
||||||
|
fun fromInt(argb: Int): DoubleArray
|
||||||
|
|
||||||
|
/** The ARGB (i.e. hex code) representation of this color. */
|
||||||
|
fun toInt(point: DoubleArray): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Squared distance between two colors. Distance is defined by scientific color spaces and
|
||||||
|
* referred to as delta E.
|
||||||
|
*/
|
||||||
|
fun distance(a: DoubleArray, b: DoubleArray): Double
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
import utils.ColorUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides conversions needed for K-Means quantization. Converting input to points, and converting
|
||||||
|
* the final state of the K-Means algorithm to colors.
|
||||||
|
*/
|
||||||
|
class PointProviderLab : PointProvider {
|
||||||
|
/**
|
||||||
|
* Convert a color represented in ARGB to a 3-element array of L*a*b* coordinates of the color.
|
||||||
|
*/
|
||||||
|
override fun fromInt(argb: Int): DoubleArray {
|
||||||
|
val lab = ColorUtils.labFromArgb(argb)
|
||||||
|
return doubleArrayOf(lab[0], lab[1], lab[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a 3-element array to a color represented in ARGB. */
|
||||||
|
override fun toInt(point: DoubleArray): Int {
|
||||||
|
return ColorUtils.argbFromLab(point[0], point[1], point[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard CIE 1976 delta E formula also takes the square root, unneeded here. This method is
|
||||||
|
* used by quantization algorithms to compare distance, and the relative ordering is the same,
|
||||||
|
* with or without a square root.
|
||||||
|
*
|
||||||
|
* This relatively minor optimization is helpful because this method is called at least once for
|
||||||
|
* each pixel in an image.
|
||||||
|
*/
|
||||||
|
override fun distance(a: DoubleArray, b: DoubleArray): Double {
|
||||||
|
val dL = a[0] - b[0]
|
||||||
|
val dA = a[1] - b[1]
|
||||||
|
val dB = a[2] - b[2]
|
||||||
|
return dL * dL + dA * dA + dB * dB
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
internal interface Quantizer {
|
||||||
|
fun quantize(pixels: IntArray, maxColors: Int): QuantizerResult
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An image quantizer that improves on the quality of a standard K-Means algorithm by setting the
|
||||||
|
* K-Means initial state to the output of a Wu quantizer, instead of random centroids. Improves on
|
||||||
|
* speed by several optimizations, as implemented in Wsmeans, or Weighted Square Means, K-Means with
|
||||||
|
* those optimizations.
|
||||||
|
*
|
||||||
|
* This algorithm was designed by M. Emre Celebi, and was found in their 2011 paper, Improving the
|
||||||
|
* Performance of K-Means for Color Quantization. https://arxiv.org/abs/1101.0395
|
||||||
|
*/
|
||||||
|
object QuantizerCelebi {
|
||||||
|
/**
|
||||||
|
* Reduce the number of colors needed to represented the input, minimizing the difference between
|
||||||
|
* the original image and the recolored image.
|
||||||
|
*
|
||||||
|
* @param pixels Colors in ARGB format.
|
||||||
|
* @param maxColors The number of colors to divide the image into. A lower number of colors may be
|
||||||
|
* returned.
|
||||||
|
* @return Map with keys of colors in ARGB format, and values of number of pixels in the original
|
||||||
|
* image that correspond to the color in the quantized image.
|
||||||
|
*/
|
||||||
|
fun quantize(pixels: IntArray, maxColors: Int): Map<Int, Int> {
|
||||||
|
val wu = QuantizerWu()
|
||||||
|
val wuResult = wu.quantize(pixels, maxColors)
|
||||||
|
val wuClustersAsObjects = wuResult.colorToCount.keys
|
||||||
|
var index = 0
|
||||||
|
val wuClusters = IntArray(wuClustersAsObjects.size)
|
||||||
|
for (argb in wuClustersAsObjects) {
|
||||||
|
wuClusters[index++] = argb
|
||||||
|
}
|
||||||
|
return QuantizerWsmeans.quantize(pixels, wuClusters, maxColors)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap
|
||||||
|
|
||||||
|
/** Creates a dictionary with keys of colors, and values of count of the color */
|
||||||
|
class QuantizerMap : Quantizer {
|
||||||
|
var colorToCount: MutableMap<Int, Int>? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun quantize(pixels: IntArray, maxColors: Int): QuantizerResult {
|
||||||
|
val pixelByCount: MutableMap<Int, Int> = LinkedHashMap()
|
||||||
|
for (pixel in pixels) {
|
||||||
|
val currentPixelCount = pixelByCount[pixel]
|
||||||
|
val newPixelCount = if (currentPixelCount == null) 1 else currentPixelCount + 1
|
||||||
|
pixelByCount[pixel] = newPixelCount
|
||||||
|
}
|
||||||
|
colorToCount = pixelByCount
|
||||||
|
return QuantizerResult(pixelByCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
/** Represents result of a quantizer run */
|
||||||
|
data class QuantizerResult(val colorToCount: Map<Int, Int>)
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
import java.util.Arrays
|
||||||
|
import java.util.LinkedHashMap
|
||||||
|
import java.util.Random
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An image quantizer that improves on the speed of a standard K-Means algorithm by implementing
|
||||||
|
* several optimizations, including deduping identical pixels and a triangle inequality rule that
|
||||||
|
* reduces the number of comparisons needed to identify which cluster a point should be moved to.
|
||||||
|
*
|
||||||
|
* Wsmeans stands for Weighted Square Means.
|
||||||
|
*
|
||||||
|
* This algorithm was designed by M. Emre Celebi, and was found in their 2011 paper, Improving the
|
||||||
|
* Performance of K-Means for Color Quantization. https://arxiv.org/abs/1101.0395
|
||||||
|
*/
|
||||||
|
object QuantizerWsmeans {
|
||||||
|
private class Distance() : Comparable<Distance> {
|
||||||
|
var index = -1
|
||||||
|
var distance = -1.0
|
||||||
|
|
||||||
|
override fun compareTo(other: Distance): Int {
|
||||||
|
return distance.compareTo(other.distance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val MAX_ITERATIONS = 10
|
||||||
|
private const val MIN_MOVEMENT_DISTANCE = 3.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce the number of colors needed to represented the input, minimizing the difference between
|
||||||
|
* the original image and the recolored image.
|
||||||
|
*
|
||||||
|
* @param inputPixels Colors in ARGB format.
|
||||||
|
* @param startingClusters Defines the initial state of the quantizer. Passing an empty array is
|
||||||
|
* fine, the implementation will create its own initial state that leads to reproducible results
|
||||||
|
* for the same inputs. Passing an array that is the result of Wu quantization leads to higher
|
||||||
|
* quality results.
|
||||||
|
* @param maxColors The number of colors to divide the image into. A lower number of colors may be
|
||||||
|
* returned.
|
||||||
|
* @return Map with keys of colors in ARGB format, values of how many of the input pixels belong
|
||||||
|
* to the color.
|
||||||
|
*/
|
||||||
|
fun quantize(inputPixels: IntArray, startingClusters: IntArray, maxColors: Int): Map<Int, Int> {
|
||||||
|
// Uses a seeded random number generator to ensure consistent results.
|
||||||
|
val random = Random(0x42688L)
|
||||||
|
val pixelToCount: MutableMap<Int, Int> = LinkedHashMap()
|
||||||
|
val points = arrayOfNulls<DoubleArray>(inputPixels.size)
|
||||||
|
val pixels = IntArray(inputPixels.size)
|
||||||
|
val pointProvider: PointProvider = PointProviderLab()
|
||||||
|
var pointCount = 0
|
||||||
|
for (i in inputPixels.indices) {
|
||||||
|
val inputPixel = inputPixels[i]
|
||||||
|
val pixelCount = pixelToCount[inputPixel]
|
||||||
|
if (pixelCount == null) {
|
||||||
|
points[pointCount] = pointProvider.fromInt(inputPixel)
|
||||||
|
pixels[pointCount] = inputPixel
|
||||||
|
pointCount++
|
||||||
|
pixelToCount[inputPixel] = 1
|
||||||
|
} else {
|
||||||
|
pixelToCount[inputPixel] = pixelCount + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val counts = IntArray(pointCount)
|
||||||
|
for (i in 0 until pointCount) {
|
||||||
|
val pixel = pixels[i]
|
||||||
|
val count = pixelToCount[pixel]!!
|
||||||
|
counts[i] = count
|
||||||
|
}
|
||||||
|
var clusterCount = min(maxColors, pointCount)
|
||||||
|
if (startingClusters.isNotEmpty()) {
|
||||||
|
clusterCount = min(clusterCount, startingClusters.size)
|
||||||
|
}
|
||||||
|
val clusters = arrayOfNulls<DoubleArray>(clusterCount)
|
||||||
|
var clustersCreated = 0
|
||||||
|
for (i in startingClusters.indices) {
|
||||||
|
clusters[i] = pointProvider.fromInt(startingClusters[i])
|
||||||
|
clustersCreated++
|
||||||
|
}
|
||||||
|
val additionalClustersNeeded = clusterCount - clustersCreated
|
||||||
|
if (additionalClustersNeeded > 0) {
|
||||||
|
for (i in 0 until additionalClustersNeeded) {}
|
||||||
|
}
|
||||||
|
val clusterIndices = IntArray(pointCount) { random.nextInt(clusterCount) }
|
||||||
|
val indexMatrix = Array(clusterCount) { IntArray(clusterCount) }
|
||||||
|
val distanceToIndexMatrix = Array(clusterCount) { Array(clusterCount) { Distance() } }
|
||||||
|
val pixelCountSums = IntArray(clusterCount)
|
||||||
|
for (iteration in 0 until MAX_ITERATIONS) {
|
||||||
|
for (i in 0 until clusterCount) {
|
||||||
|
for (j in i + 1 until clusterCount) {
|
||||||
|
val distance = pointProvider.distance(clusters[i]!!, clusters[j]!!)
|
||||||
|
distanceToIndexMatrix[j][i].distance = distance
|
||||||
|
distanceToIndexMatrix[j][i].index = i
|
||||||
|
distanceToIndexMatrix[i][j].distance = distance
|
||||||
|
distanceToIndexMatrix[i][j].index = j
|
||||||
|
}
|
||||||
|
Arrays.sort(distanceToIndexMatrix[i])
|
||||||
|
for (j in 0 until clusterCount) {
|
||||||
|
indexMatrix[i][j] = distanceToIndexMatrix[i][j].index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var pointsMoved = 0
|
||||||
|
for (i in 0 until pointCount) {
|
||||||
|
val point = points[i]!!
|
||||||
|
val previousClusterIndex = clusterIndices[i]
|
||||||
|
val previousCluster = clusters[previousClusterIndex]!!
|
||||||
|
val previousDistance = pointProvider.distance(point, previousCluster)
|
||||||
|
var minimumDistance = previousDistance
|
||||||
|
var newClusterIndex = -1
|
||||||
|
for (j in 0 until clusterCount) {
|
||||||
|
if (distanceToIndexMatrix[previousClusterIndex][j].distance >= 4 * previousDistance) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val distance = pointProvider.distance(point, clusters[j]!!)
|
||||||
|
if (distance < minimumDistance) {
|
||||||
|
minimumDistance = distance
|
||||||
|
newClusterIndex = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newClusterIndex != -1) {
|
||||||
|
val distanceChange = abs(sqrt(minimumDistance) - sqrt(previousDistance))
|
||||||
|
if (distanceChange > MIN_MOVEMENT_DISTANCE) {
|
||||||
|
pointsMoved++
|
||||||
|
clusterIndices[i] = newClusterIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pointsMoved == 0 && iteration != 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val componentASums = DoubleArray(clusterCount)
|
||||||
|
val componentBSums = DoubleArray(clusterCount)
|
||||||
|
val componentCSums = DoubleArray(clusterCount)
|
||||||
|
pixelCountSums.fill(0)
|
||||||
|
for (i in 0 until pointCount) {
|
||||||
|
val clusterIndex = clusterIndices[i]
|
||||||
|
val point = points[i]!!
|
||||||
|
val count = counts[i]
|
||||||
|
pixelCountSums[clusterIndex] += count
|
||||||
|
componentASums[clusterIndex] += point[0] * count
|
||||||
|
componentBSums[clusterIndex] += point[1] * count
|
||||||
|
componentCSums[clusterIndex] += point[2] * count
|
||||||
|
}
|
||||||
|
for (i in 0 until clusterCount) {
|
||||||
|
val count = pixelCountSums[i]
|
||||||
|
if (count == 0) {
|
||||||
|
clusters[i] = doubleArrayOf(0.0, 0.0, 0.0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val a = componentASums[i] / count
|
||||||
|
val b = componentBSums[i] / count
|
||||||
|
val c = componentCSums[i] / count
|
||||||
|
clusters[i] = doubleArrayOf(a, b, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val argbToPopulation: MutableMap<Int, Int> = LinkedHashMap()
|
||||||
|
for (i in 0 until clusterCount) {
|
||||||
|
val count = pixelCountSums[i]
|
||||||
|
if (count == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val possibleNewCluster = pointProvider.toInt(clusters[i]!!)
|
||||||
|
if (argbToPopulation.containsKey(possibleNewCluster)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
argbToPopulation[possibleNewCluster] = count
|
||||||
|
}
|
||||||
|
return argbToPopulation
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package quantize
|
||||||
|
|
||||||
|
import utils.ColorUtils
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.LinkedHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An image quantizer that divides the image's pixels into clusters by recursively cutting an RGB
|
||||||
|
* cube, based on the weight of pixels in each area of the cube.
|
||||||
|
*
|
||||||
|
* The algorithm was described by Xiaolin Wu in Graphic Gems II, published in 1991.
|
||||||
|
*/
|
||||||
|
class QuantizerWu : Quantizer {
|
||||||
|
private lateinit var weights: IntArray
|
||||||
|
private lateinit var momentsR: IntArray
|
||||||
|
private lateinit var momentsG: IntArray
|
||||||
|
private lateinit var momentsB: IntArray
|
||||||
|
private lateinit var moments: DoubleArray
|
||||||
|
private lateinit var cubes: Array<Box>
|
||||||
|
|
||||||
|
override fun quantize(pixels: IntArray, maxColors: Int): QuantizerResult {
|
||||||
|
val mapResult = QuantizerMap().quantize(pixels, maxColors)
|
||||||
|
constructHistogram(mapResult.colorToCount)
|
||||||
|
createMoments()
|
||||||
|
val createBoxesResult = createBoxes(maxColors)
|
||||||
|
val colors = createResult(createBoxesResult.resultCount)
|
||||||
|
val resultMap: MutableMap<Int, Int> = LinkedHashMap()
|
||||||
|
for (color in colors) {
|
||||||
|
resultMap[color] = 0
|
||||||
|
}
|
||||||
|
return QuantizerResult(resultMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun constructHistogram(pixels: Map<Int, Int>) {
|
||||||
|
weights = IntArray(TOTAL_SIZE)
|
||||||
|
momentsR = IntArray(TOTAL_SIZE)
|
||||||
|
momentsG = IntArray(TOTAL_SIZE)
|
||||||
|
momentsB = IntArray(TOTAL_SIZE)
|
||||||
|
moments = DoubleArray(TOTAL_SIZE)
|
||||||
|
for ((pixel, count) in pixels) {
|
||||||
|
val red = ColorUtils.redFromArgb(pixel)
|
||||||
|
val green = ColorUtils.greenFromArgb(pixel)
|
||||||
|
val blue = ColorUtils.blueFromArgb(pixel)
|
||||||
|
val bitsToRemove = 8 - INDEX_BITS
|
||||||
|
val iR = (red shr bitsToRemove) + 1
|
||||||
|
val iG = (green shr bitsToRemove) + 1
|
||||||
|
val iB = (blue shr bitsToRemove) + 1
|
||||||
|
val index = getIndex(iR, iG, iB)
|
||||||
|
weights[index] += count
|
||||||
|
momentsR[index] += red * count
|
||||||
|
momentsG[index] += green * count
|
||||||
|
momentsB[index] += blue * count
|
||||||
|
moments[index] += count * (red * red + green * green + blue * blue).toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createMoments() {
|
||||||
|
for (r in 1 until INDEX_COUNT) {
|
||||||
|
val area = IntArray(INDEX_COUNT)
|
||||||
|
val areaR = IntArray(INDEX_COUNT)
|
||||||
|
val areaG = IntArray(INDEX_COUNT)
|
||||||
|
val areaB = IntArray(INDEX_COUNT)
|
||||||
|
val area2 = DoubleArray(INDEX_COUNT)
|
||||||
|
for (g in 1 until INDEX_COUNT) {
|
||||||
|
var line = 0
|
||||||
|
var lineR = 0
|
||||||
|
var lineG = 0
|
||||||
|
var lineB = 0
|
||||||
|
var line2 = 0.0
|
||||||
|
for (b in 1 until INDEX_COUNT) {
|
||||||
|
val index = getIndex(r, g, b)
|
||||||
|
line += weights[index]
|
||||||
|
lineR += momentsR[index]
|
||||||
|
lineG += momentsG[index]
|
||||||
|
lineB += momentsB[index]
|
||||||
|
line2 += moments[index]
|
||||||
|
area[b] += line
|
||||||
|
areaR[b] += lineR
|
||||||
|
areaG[b] += lineG
|
||||||
|
areaB[b] += lineB
|
||||||
|
area2[b] += line2
|
||||||
|
val previousIndex = getIndex(r - 1, g, b)
|
||||||
|
weights[index] = weights[previousIndex] + area[b]
|
||||||
|
momentsR[index] = momentsR[previousIndex] + areaR[b]
|
||||||
|
momentsG[index] = momentsG[previousIndex] + areaG[b]
|
||||||
|
momentsB[index] = momentsB[previousIndex] + areaB[b]
|
||||||
|
moments[index] = moments[previousIndex] + area2[b]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun createBoxes(maxColorCount: Int): CreateBoxesResult {
|
||||||
|
cubes = Array(maxColorCount) { Box() }
|
||||||
|
val volumeVariance = DoubleArray(maxColorCount)
|
||||||
|
val firstBox = cubes[0]
|
||||||
|
firstBox.r1 = INDEX_COUNT - 1
|
||||||
|
firstBox.g1 = INDEX_COUNT - 1
|
||||||
|
firstBox.b1 = INDEX_COUNT - 1
|
||||||
|
var generatedColorCount = maxColorCount
|
||||||
|
var next = 0
|
||||||
|
var i = 1
|
||||||
|
while (i < maxColorCount) {
|
||||||
|
if (cut(cubes[next], cubes[i])) {
|
||||||
|
volumeVariance[next] = if (cubes[next].vol > 1) variance(cubes[next]) else 0.0
|
||||||
|
volumeVariance[i] = if (cubes[i].vol > 1) variance(cubes[i]) else 0.0
|
||||||
|
} else {
|
||||||
|
volumeVariance[next] = 0.0
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
next = 0
|
||||||
|
var temp = volumeVariance[0]
|
||||||
|
for (j in 1..i) {
|
||||||
|
if (volumeVariance[j] > temp) {
|
||||||
|
temp = volumeVariance[j]
|
||||||
|
next = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (temp <= 0.0) {
|
||||||
|
generatedColorCount = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return CreateBoxesResult(maxColorCount, generatedColorCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createResult(colorCount: Int): List<Int> {
|
||||||
|
val colors: MutableList<Int> = ArrayList()
|
||||||
|
for (i in 0 until colorCount) {
|
||||||
|
val cube = cubes[i]
|
||||||
|
val weight = volume(cube, weights)
|
||||||
|
if (weight > 0) {
|
||||||
|
val r = volume(cube, momentsR) / weight
|
||||||
|
val g = volume(cube, momentsG) / weight
|
||||||
|
val b = volume(cube, momentsB) / weight
|
||||||
|
val color = 255 shl 24 or (r and 0x0ff shl 16) or (g and 0x0ff shl 8) or (b and 0x0ff)
|
||||||
|
colors.add(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colors
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun variance(cube: Box): Double {
|
||||||
|
val dr = volume(cube, momentsR)
|
||||||
|
val dg = volume(cube, momentsG)
|
||||||
|
val db = volume(cube, momentsB)
|
||||||
|
val xx =
|
||||||
|
(moments[getIndex(cube.r1, cube.g1, cube.b1)] -
|
||||||
|
moments[getIndex(cube.r1, cube.g1, cube.b0)] -
|
||||||
|
moments[getIndex(cube.r1, cube.g0, cube.b1)] +
|
||||||
|
moments[getIndex(cube.r1, cube.g0, cube.b0)] -
|
||||||
|
moments[getIndex(cube.r0, cube.g1, cube.b1)] +
|
||||||
|
moments[getIndex(cube.r0, cube.g1, cube.b0)] +
|
||||||
|
moments[getIndex(cube.r0, cube.g0, cube.b1)] - moments[getIndex(cube.r0, cube.g0, cube.b0)])
|
||||||
|
val hypotenuse = dr * dr + dg * dg + db * db
|
||||||
|
val volume = volume(cube, weights)
|
||||||
|
return xx - hypotenuse / volume.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun cut(one: Box, two: Box): Boolean {
|
||||||
|
val wholeR = volume(one, momentsR)
|
||||||
|
val wholeG = volume(one, momentsG)
|
||||||
|
val wholeB = volume(one, momentsB)
|
||||||
|
val wholeW = volume(one, weights)
|
||||||
|
val maxRResult =
|
||||||
|
maximize(one, Direction.RED, one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW)
|
||||||
|
val maxGResult =
|
||||||
|
maximize(one, Direction.GREEN, one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW)
|
||||||
|
val maxBResult =
|
||||||
|
maximize(one, Direction.BLUE, one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW)
|
||||||
|
val maxR = maxRResult.maximum
|
||||||
|
val maxG = maxGResult.maximum
|
||||||
|
val maxB = maxBResult.maximum
|
||||||
|
val cutDirection =
|
||||||
|
when {
|
||||||
|
maxR >= maxG && maxR >= maxB -> {
|
||||||
|
if (maxRResult.cutLocation < 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Direction.RED
|
||||||
|
}
|
||||||
|
maxG >= maxR && maxG >= maxB -> Direction.GREEN
|
||||||
|
else -> Direction.BLUE
|
||||||
|
}
|
||||||
|
two.r1 = one.r1
|
||||||
|
two.g1 = one.g1
|
||||||
|
two.b1 = one.b1
|
||||||
|
when (cutDirection) {
|
||||||
|
Direction.RED -> {
|
||||||
|
one.r1 = maxRResult.cutLocation
|
||||||
|
two.r0 = one.r1
|
||||||
|
two.g0 = one.g0
|
||||||
|
two.b0 = one.b0
|
||||||
|
}
|
||||||
|
Direction.GREEN -> {
|
||||||
|
one.g1 = maxGResult.cutLocation
|
||||||
|
two.r0 = one.r0
|
||||||
|
two.g0 = one.g1
|
||||||
|
two.b0 = one.b0
|
||||||
|
}
|
||||||
|
Direction.BLUE -> {
|
||||||
|
one.b1 = maxBResult.cutLocation
|
||||||
|
two.r0 = one.r0
|
||||||
|
two.g0 = one.g0
|
||||||
|
two.b0 = one.b1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0)
|
||||||
|
two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun maximize(
|
||||||
|
cube: Box,
|
||||||
|
direction: Direction,
|
||||||
|
first: Int,
|
||||||
|
last: Int,
|
||||||
|
wholeR: Int,
|
||||||
|
wholeG: Int,
|
||||||
|
wholeB: Int,
|
||||||
|
wholeW: Int,
|
||||||
|
): MaximizeResult {
|
||||||
|
val bottomR = bottom(cube, direction, momentsR)
|
||||||
|
val bottomG = bottom(cube, direction, momentsG)
|
||||||
|
val bottomB = bottom(cube, direction, momentsB)
|
||||||
|
val bottomW = bottom(cube, direction, weights)
|
||||||
|
var max = 0.0
|
||||||
|
var cut = -1
|
||||||
|
var halfR: Int
|
||||||
|
var halfG: Int
|
||||||
|
var halfB: Int
|
||||||
|
var halfW: Int
|
||||||
|
for (i in first until last) {
|
||||||
|
halfR = bottomR + top(cube, direction, i, momentsR)
|
||||||
|
halfG = bottomG + top(cube, direction, i, momentsG)
|
||||||
|
halfB = bottomB + top(cube, direction, i, momentsB)
|
||||||
|
halfW = bottomW + top(cube, direction, i, weights)
|
||||||
|
if (halfW == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var tempNumerator = (halfR * halfR + halfG * halfG + halfB * halfB).toDouble()
|
||||||
|
var tempDenominator = halfW.toDouble()
|
||||||
|
var temp = tempNumerator / tempDenominator
|
||||||
|
halfR = wholeR - halfR
|
||||||
|
halfG = wholeG - halfG
|
||||||
|
halfB = wholeB - halfB
|
||||||
|
halfW = wholeW - halfW
|
||||||
|
if (halfW == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tempNumerator = (halfR * halfR + halfG * halfG + halfB * halfB).toDouble()
|
||||||
|
tempDenominator = halfW.toDouble()
|
||||||
|
temp += tempNumerator / tempDenominator
|
||||||
|
if (temp > max) {
|
||||||
|
max = temp
|
||||||
|
cut = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MaximizeResult(cut, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class Direction {
|
||||||
|
RED,
|
||||||
|
GREEN,
|
||||||
|
BLUE,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MaximizeResult(
|
||||||
|
// < 0 if cut impossible
|
||||||
|
var cutLocation: Int,
|
||||||
|
var maximum: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal class CreateBoxesResult(var requestedCount: Int, var resultCount: Int)
|
||||||
|
|
||||||
|
internal class Box {
|
||||||
|
var r0 = 0
|
||||||
|
var r1 = 0
|
||||||
|
var g0 = 0
|
||||||
|
var g1 = 0
|
||||||
|
var b0 = 0
|
||||||
|
var b1 = 0
|
||||||
|
var vol = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// A histogram of all the input colors is constructed. It has the shape of a cube. The cube
|
||||||
|
// would be too large if it contained all 16 million colors: historical best practice is to use
|
||||||
|
// 5 bits of the 8 in each channel, reducing the histogram to a volume of ~32,000.
|
||||||
|
private const val INDEX_BITS = 5
|
||||||
|
private const val INDEX_COUNT = 33 // ((1 << INDEX_BITS) + 1)
|
||||||
|
private const val TOTAL_SIZE = 35937 // INDEX_COUNT * INDEX_COUNT * INDEX_COUNT
|
||||||
|
|
||||||
|
fun getIndex(r: Int, g: Int, b: Int): Int {
|
||||||
|
return (r shl (INDEX_BITS * 2)) + (r shl (INDEX_BITS + 1)) + r + (g shl INDEX_BITS) + g + b
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun volume(cube: Box, moment: IntArray): Int {
|
||||||
|
return (moment[getIndex(cube.r1, cube.g1, cube.b1)] -
|
||||||
|
moment[getIndex(cube.r1, cube.g1, cube.b0)] -
|
||||||
|
moment[getIndex(cube.r1, cube.g0, cube.b1)] + moment[getIndex(cube.r1, cube.g0, cube.b0)] -
|
||||||
|
moment[getIndex(cube.r0, cube.g1, cube.b1)] +
|
||||||
|
moment[getIndex(cube.r0, cube.g1, cube.b0)] +
|
||||||
|
moment[getIndex(cube.r0, cube.g0, cube.b1)] - moment[getIndex(cube.r0, cube.g0, cube.b0)])
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun bottom(cube: Box, direction: Direction, moment: IntArray): Int {
|
||||||
|
return when (direction) {
|
||||||
|
Direction.RED ->
|
||||||
|
-moment[getIndex(cube.r0, cube.g1, cube.b1)] +
|
||||||
|
moment[getIndex(cube.r0, cube.g1, cube.b0)] +
|
||||||
|
moment[getIndex(cube.r0, cube.g0, cube.b1)] -
|
||||||
|
moment[getIndex(cube.r0, cube.g0, cube.b0)]
|
||||||
|
Direction.GREEN ->
|
||||||
|
-moment[getIndex(cube.r1, cube.g0, cube.b1)] +
|
||||||
|
moment[getIndex(cube.r1, cube.g0, cube.b0)] +
|
||||||
|
moment[getIndex(cube.r0, cube.g0, cube.b1)] -
|
||||||
|
moment[getIndex(cube.r0, cube.g0, cube.b0)]
|
||||||
|
Direction.BLUE ->
|
||||||
|
-moment[getIndex(cube.r1, cube.g1, cube.b0)] +
|
||||||
|
moment[getIndex(cube.r1, cube.g0, cube.b0)] +
|
||||||
|
moment[getIndex(cube.r0, cube.g1, cube.b0)] -
|
||||||
|
moment[getIndex(cube.r0, cube.g0, cube.b0)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun top(cube: Box, direction: Direction, position: Int, moment: IntArray): Int {
|
||||||
|
return when (direction) {
|
||||||
|
Direction.RED ->
|
||||||
|
(moment[getIndex(position, cube.g1, cube.b1)] -
|
||||||
|
moment[getIndex(position, cube.g1, cube.b0)] -
|
||||||
|
moment[getIndex(position, cube.g0, cube.b1)] +
|
||||||
|
moment[getIndex(position, cube.g0, cube.b0)])
|
||||||
|
Direction.GREEN ->
|
||||||
|
(moment[getIndex(cube.r1, position, cube.b1)] -
|
||||||
|
moment[getIndex(cube.r1, position, cube.b0)] -
|
||||||
|
moment[getIndex(cube.r0, position, cube.b1)] +
|
||||||
|
moment[getIndex(cube.r0, position, cube.b0)])
|
||||||
|
Direction.BLUE ->
|
||||||
|
(moment[getIndex(cube.r1, cube.g1, position)] -
|
||||||
|
moment[getIndex(cube.r1, cube.g0, position)] -
|
||||||
|
moment[getIndex(cube.r0, cube.g1, position)] +
|
||||||
|
moment[getIndex(cube.r0, cube.g0, position)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,953 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This file is automatically generated. Do not modify it.
|
|
||||||
|
|
||||||
package scheme;
|
|
||||||
|
|
||||||
import palettes.CorePalette;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a Material color scheme, a mapping of color roles to colors.
|
|
||||||
*/
|
|
||||||
public class Scheme {
|
|
||||||
private int primary;
|
|
||||||
private int onPrimary;
|
|
||||||
private int primaryContainer;
|
|
||||||
private int onPrimaryContainer;
|
|
||||||
private int secondary;
|
|
||||||
private int onSecondary;
|
|
||||||
private int secondaryContainer;
|
|
||||||
private int onSecondaryContainer;
|
|
||||||
private int tertiary;
|
|
||||||
private int onTertiary;
|
|
||||||
private int tertiaryContainer;
|
|
||||||
private int onTertiaryContainer;
|
|
||||||
private int error;
|
|
||||||
private int onError;
|
|
||||||
private int errorContainer;
|
|
||||||
private int onErrorContainer;
|
|
||||||
private int background;
|
|
||||||
private int onBackground;
|
|
||||||
private int surface;
|
|
||||||
private int onSurface;
|
|
||||||
private int surfaceVariant;
|
|
||||||
private int onSurfaceVariant;
|
|
||||||
private int outline;
|
|
||||||
private int outlineVariant;
|
|
||||||
private int shadow;
|
|
||||||
private int scrim;
|
|
||||||
private int inverseSurface;
|
|
||||||
private int inverseOnSurface;
|
|
||||||
private int inversePrimary;
|
|
||||||
private int surfaceDim;
|
|
||||||
private int surfaceBright;
|
|
||||||
private int surfaceContainerLowest;
|
|
||||||
private int surfaceContainerLow;
|
|
||||||
private int surfaceContainer;
|
|
||||||
private int surfaceContainerHigh;
|
|
||||||
private int surfaceContainerHighest;
|
|
||||||
|
|
||||||
public Scheme() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme(
|
|
||||||
int primary,
|
|
||||||
int onPrimary,
|
|
||||||
int primaryContainer,
|
|
||||||
int onPrimaryContainer,
|
|
||||||
int secondary,
|
|
||||||
int onSecondary,
|
|
||||||
int secondaryContainer,
|
|
||||||
int onSecondaryContainer,
|
|
||||||
int tertiary,
|
|
||||||
int onTertiary,
|
|
||||||
int tertiaryContainer,
|
|
||||||
int onTertiaryContainer,
|
|
||||||
int error,
|
|
||||||
int onError,
|
|
||||||
int errorContainer,
|
|
||||||
int onErrorContainer,
|
|
||||||
int background,
|
|
||||||
int onBackground,
|
|
||||||
int surface,
|
|
||||||
int onSurface,
|
|
||||||
int surfaceVariant,
|
|
||||||
int onSurfaceVariant,
|
|
||||||
int outline,
|
|
||||||
int outlineVariant,
|
|
||||||
int shadow,
|
|
||||||
int scrim,
|
|
||||||
int inverseSurface,
|
|
||||||
int inverseOnSurface,
|
|
||||||
int inversePrimary,
|
|
||||||
int surfaceDim,
|
|
||||||
int surfaceBright,
|
|
||||||
int surfaceContainerLowest,
|
|
||||||
int surfaceContainerLow,
|
|
||||||
int surfaceContainer,
|
|
||||||
int surfaceContainerHigh,
|
|
||||||
int surfaceContainerHighest
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.primary = primary;
|
|
||||||
this.onPrimary = onPrimary;
|
|
||||||
this.primaryContainer = primaryContainer;
|
|
||||||
this.onPrimaryContainer = onPrimaryContainer;
|
|
||||||
this.secondary = secondary;
|
|
||||||
this.onSecondary = onSecondary;
|
|
||||||
this.secondaryContainer = secondaryContainer;
|
|
||||||
this.onSecondaryContainer = onSecondaryContainer;
|
|
||||||
this.tertiary = tertiary;
|
|
||||||
this.onTertiary = onTertiary;
|
|
||||||
this.tertiaryContainer = tertiaryContainer;
|
|
||||||
this.onTertiaryContainer = onTertiaryContainer;
|
|
||||||
this.error = error;
|
|
||||||
this.onError = onError;
|
|
||||||
this.errorContainer = errorContainer;
|
|
||||||
this.onErrorContainer = onErrorContainer;
|
|
||||||
this.background = background;
|
|
||||||
this.onBackground = onBackground;
|
|
||||||
this.surface = surface;
|
|
||||||
this.onSurface = onSurface;
|
|
||||||
this.surfaceVariant = surfaceVariant;
|
|
||||||
this.onSurfaceVariant = onSurfaceVariant;
|
|
||||||
this.outline = outline;
|
|
||||||
this.outlineVariant = outlineVariant;
|
|
||||||
this.shadow = shadow;
|
|
||||||
this.scrim = scrim;
|
|
||||||
this.inverseSurface = inverseSurface;
|
|
||||||
this.inverseOnSurface = inverseOnSurface;
|
|
||||||
this.inversePrimary = inversePrimary;
|
|
||||||
this.surfaceDim = surfaceDim;
|
|
||||||
this.surfaceBright = surfaceBright;
|
|
||||||
this.surfaceContainerLowest = surfaceContainerLowest;
|
|
||||||
this.surfaceContainerLow = surfaceContainerLow;
|
|
||||||
this.surfaceContainer = surfaceContainer;
|
|
||||||
this.surfaceContainerHigh = surfaceContainerHigh;
|
|
||||||
this.surfaceContainerHighest = surfaceContainerHighest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Scheme light(int argb) {
|
|
||||||
return lightFromCorePalette(CorePalette.of(argb));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Scheme dark(int argb) {
|
|
||||||
return darkFromCorePalette(CorePalette.of(argb));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Scheme lightContent(int argb) {
|
|
||||||
return lightFromCorePalette(CorePalette.contentOf(argb));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Scheme darkContent(int argb) {
|
|
||||||
return darkFromCorePalette(CorePalette.contentOf(argb));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Scheme lightFromCorePalette(CorePalette core) {
|
|
||||||
return new Scheme()
|
|
||||||
.withPrimary(core.a1.tone(40))
|
|
||||||
.withOnPrimary(core.a1.tone(100))
|
|
||||||
.withPrimaryContainer(core.a1.tone(90))
|
|
||||||
.withOnPrimaryContainer(core.a1.tone(10))
|
|
||||||
.withSecondary(core.a2.tone(40))
|
|
||||||
.withOnSecondary(core.a2.tone(100))
|
|
||||||
.withSecondaryContainer(core.a2.tone(90))
|
|
||||||
.withOnSecondaryContainer(core.a2.tone(10))
|
|
||||||
.withTertiary(core.a3.tone(40))
|
|
||||||
.withOnTertiary(core.a3.tone(100))
|
|
||||||
.withTertiaryContainer(core.a3.tone(90))
|
|
||||||
.withOnTertiaryContainer(core.a3.tone(10))
|
|
||||||
.withError(core.error.tone(40))
|
|
||||||
.withOnError(core.error.tone(100))
|
|
||||||
.withErrorContainer(core.error.tone(90))
|
|
||||||
.withOnErrorContainer(core.error.tone(10))
|
|
||||||
.withBackground(core.n1.tone(99))
|
|
||||||
.withOnBackground(core.n1.tone(10))
|
|
||||||
.withSurface(core.n1.tone(98))
|
|
||||||
.withOnSurface(core.n1.tone(10))
|
|
||||||
.withSurfaceVariant(core.n2.tone(90))
|
|
||||||
.withOnSurfaceVariant(core.n2.tone(30))
|
|
||||||
.withOutline(core.n2.tone(50))
|
|
||||||
.withOutlineVariant(core.n2.tone(80))
|
|
||||||
.withShadow(core.n1.tone(0))
|
|
||||||
.withScrim(core.n1.tone(0))
|
|
||||||
.withInverseSurface(core.n1.tone(20))
|
|
||||||
.withInverseOnSurface(core.n1.tone(95))
|
|
||||||
.withInversePrimary(core.a1.tone(80))
|
|
||||||
.withSurfaceDim(core.n1.tone(87))
|
|
||||||
.withSurfaceBright(core.n1.tone(98))
|
|
||||||
.withSurfaceContainerLowest(core.n1.tone(100))
|
|
||||||
.withSurfaceContainerLow(core.n1.tone(96))
|
|
||||||
.withSurfaceContainer(core.n1.tone(94))
|
|
||||||
.withSurfaceContainerHigh(core.n1.tone(92))
|
|
||||||
.withSurfaceContainerHighest(core.n1.tone(90));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Scheme darkFromCorePalette(CorePalette core) {
|
|
||||||
return new Scheme()
|
|
||||||
.withPrimary(core.a1.tone(80))
|
|
||||||
.withOnPrimary(core.a1.tone(20))
|
|
||||||
.withPrimaryContainer(core.a1.tone(30))
|
|
||||||
.withOnPrimaryContainer(core.a1.tone(90))
|
|
||||||
.withSecondary(core.a2.tone(80))
|
|
||||||
.withOnSecondary(core.a2.tone(20))
|
|
||||||
.withSecondaryContainer(core.a2.tone(30))
|
|
||||||
.withOnSecondaryContainer(core.a2.tone(90))
|
|
||||||
.withTertiary(core.a3.tone(80))
|
|
||||||
.withOnTertiary(core.a3.tone(20))
|
|
||||||
.withTertiaryContainer(core.a3.tone(30))
|
|
||||||
.withOnTertiaryContainer(core.a3.tone(90))
|
|
||||||
.withError(core.error.tone(80))
|
|
||||||
.withOnError(core.error.tone(20))
|
|
||||||
.withErrorContainer(core.error.tone(30))
|
|
||||||
.withOnErrorContainer(core.error.tone(80))
|
|
||||||
.withBackground(core.n1.tone(10))
|
|
||||||
.withOnBackground(core.n1.tone(90))
|
|
||||||
.withSurface(core.n1.tone(6))
|
|
||||||
.withOnSurface(core.n1.tone(90))
|
|
||||||
.withSurfaceVariant(core.n2.tone(30))
|
|
||||||
.withOnSurfaceVariant(core.n2.tone(80))
|
|
||||||
.withOutline(core.n2.tone(60))
|
|
||||||
.withOutlineVariant(core.n2.tone(30))
|
|
||||||
.withShadow(core.n1.tone(0))
|
|
||||||
.withScrim(core.n1.tone(0))
|
|
||||||
.withInverseSurface(core.n1.tone(90))
|
|
||||||
.withInverseOnSurface(core.n1.tone(20))
|
|
||||||
.withInversePrimary(core.a1.tone(40))
|
|
||||||
.withSurfaceDim(core.n1.tone(6))
|
|
||||||
.withSurfaceBright(core.n1.tone(24))
|
|
||||||
.withSurfaceContainerLowest(core.n1.tone(4))
|
|
||||||
.withSurfaceContainerLow(core.n1.tone(10))
|
|
||||||
.withSurfaceContainer(core.n1.tone(12))
|
|
||||||
.withSurfaceContainerHigh(core.n1.tone(17))
|
|
||||||
.withSurfaceContainerHighest(core.n1.tone(22));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPrimary() {
|
|
||||||
return primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPrimary(int primary) {
|
|
||||||
this.primary = primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withPrimary(int primary) {
|
|
||||||
this.primary = primary;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnPrimary() {
|
|
||||||
return onPrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnPrimary(int onPrimary) {
|
|
||||||
this.onPrimary = onPrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnPrimary(int onPrimary) {
|
|
||||||
this.onPrimary = onPrimary;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPrimaryContainer() {
|
|
||||||
return primaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPrimaryContainer(int primaryContainer) {
|
|
||||||
this.primaryContainer = primaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withPrimaryContainer(int primaryContainer) {
|
|
||||||
this.primaryContainer = primaryContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnPrimaryContainer() {
|
|
||||||
return onPrimaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnPrimaryContainer(int onPrimaryContainer) {
|
|
||||||
this.onPrimaryContainer = onPrimaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnPrimaryContainer(int onPrimaryContainer) {
|
|
||||||
this.onPrimaryContainer = onPrimaryContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSecondary() {
|
|
||||||
return secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSecondary(int secondary) {
|
|
||||||
this.secondary = secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSecondary(int secondary) {
|
|
||||||
this.secondary = secondary;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnSecondary() {
|
|
||||||
return onSecondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnSecondary(int onSecondary) {
|
|
||||||
this.onSecondary = onSecondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnSecondary(int onSecondary) {
|
|
||||||
this.onSecondary = onSecondary;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSecondaryContainer() {
|
|
||||||
return secondaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSecondaryContainer(int secondaryContainer) {
|
|
||||||
this.secondaryContainer = secondaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSecondaryContainer(int secondaryContainer) {
|
|
||||||
this.secondaryContainer = secondaryContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnSecondaryContainer() {
|
|
||||||
return onSecondaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnSecondaryContainer(int onSecondaryContainer) {
|
|
||||||
this.onSecondaryContainer = onSecondaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnSecondaryContainer(int onSecondaryContainer) {
|
|
||||||
this.onSecondaryContainer = onSecondaryContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTertiary() {
|
|
||||||
return tertiary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTertiary(int tertiary) {
|
|
||||||
this.tertiary = tertiary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withTertiary(int tertiary) {
|
|
||||||
this.tertiary = tertiary;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnTertiary() {
|
|
||||||
return onTertiary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnTertiary(int onTertiary) {
|
|
||||||
this.onTertiary = onTertiary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnTertiary(int onTertiary) {
|
|
||||||
this.onTertiary = onTertiary;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTertiaryContainer() {
|
|
||||||
return tertiaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTertiaryContainer(int tertiaryContainer) {
|
|
||||||
this.tertiaryContainer = tertiaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withTertiaryContainer(int tertiaryContainer) {
|
|
||||||
this.tertiaryContainer = tertiaryContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnTertiaryContainer() {
|
|
||||||
return onTertiaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnTertiaryContainer(int onTertiaryContainer) {
|
|
||||||
this.onTertiaryContainer = onTertiaryContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnTertiaryContainer(int onTertiaryContainer) {
|
|
||||||
this.onTertiaryContainer = onTertiaryContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getError() {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setError(int error) {
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withError(int error) {
|
|
||||||
this.error = error;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnError() {
|
|
||||||
return onError;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnError(int onError) {
|
|
||||||
this.onError = onError;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnError(int onError) {
|
|
||||||
this.onError = onError;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getErrorContainer() {
|
|
||||||
return errorContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setErrorContainer(int errorContainer) {
|
|
||||||
this.errorContainer = errorContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withErrorContainer(int errorContainer) {
|
|
||||||
this.errorContainer = errorContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnErrorContainer() {
|
|
||||||
return onErrorContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnErrorContainer(int onErrorContainer) {
|
|
||||||
this.onErrorContainer = onErrorContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnErrorContainer(int onErrorContainer) {
|
|
||||||
this.onErrorContainer = onErrorContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getBackground() {
|
|
||||||
return background;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBackground(int background) {
|
|
||||||
this.background = background;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withBackground(int background) {
|
|
||||||
this.background = background;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnBackground() {
|
|
||||||
return onBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnBackground(int onBackground) {
|
|
||||||
this.onBackground = onBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnBackground(int onBackground) {
|
|
||||||
this.onBackground = onBackground;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurface() {
|
|
||||||
return surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurface(int surface) {
|
|
||||||
this.surface = surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurface(int surface) {
|
|
||||||
this.surface = surface;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnSurface() {
|
|
||||||
return onSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnSurface(int onSurface) {
|
|
||||||
this.onSurface = onSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnSurface(int onSurface) {
|
|
||||||
this.onSurface = onSurface;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceVariant() {
|
|
||||||
return surfaceVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceVariant(int surfaceVariant) {
|
|
||||||
this.surfaceVariant = surfaceVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceVariant(int surfaceVariant) {
|
|
||||||
this.surfaceVariant = surfaceVariant;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOnSurfaceVariant() {
|
|
||||||
return onSurfaceVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnSurfaceVariant(int onSurfaceVariant) {
|
|
||||||
this.onSurfaceVariant = onSurfaceVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOnSurfaceVariant(int onSurfaceVariant) {
|
|
||||||
this.onSurfaceVariant = onSurfaceVariant;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOutline() {
|
|
||||||
return outline;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOutline(int outline) {
|
|
||||||
this.outline = outline;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOutline(int outline) {
|
|
||||||
this.outline = outline;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOutlineVariant() {
|
|
||||||
return outlineVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOutlineVariant(int outlineVariant) {
|
|
||||||
this.outlineVariant = outlineVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withOutlineVariant(int outlineVariant) {
|
|
||||||
this.outlineVariant = outlineVariant;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getShadow() {
|
|
||||||
return shadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setShadow(int shadow) {
|
|
||||||
this.shadow = shadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withShadow(int shadow) {
|
|
||||||
this.shadow = shadow;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getScrim() {
|
|
||||||
return scrim;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setScrim(int scrim) {
|
|
||||||
this.scrim = scrim;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withScrim(int scrim) {
|
|
||||||
this.scrim = scrim;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getInverseSurface() {
|
|
||||||
return inverseSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInverseSurface(int inverseSurface) {
|
|
||||||
this.inverseSurface = inverseSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withInverseSurface(int inverseSurface) {
|
|
||||||
this.inverseSurface = inverseSurface;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getInverseOnSurface() {
|
|
||||||
return inverseOnSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInverseOnSurface(int inverseOnSurface) {
|
|
||||||
this.inverseOnSurface = inverseOnSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withInverseOnSurface(int inverseOnSurface) {
|
|
||||||
this.inverseOnSurface = inverseOnSurface;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getInversePrimary() {
|
|
||||||
return inversePrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInversePrimary(int inversePrimary) {
|
|
||||||
this.inversePrimary = inversePrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withInversePrimary(int inversePrimary) {
|
|
||||||
this.inversePrimary = inversePrimary;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceDim() {
|
|
||||||
return surfaceDim;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceDim(int surfaceDim) {
|
|
||||||
this.surfaceDim = surfaceDim;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceDim(int surfaceDim) {
|
|
||||||
this.surfaceDim = surfaceDim;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceBright() {
|
|
||||||
return surfaceBright;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceBright(int surfaceBright) {
|
|
||||||
this.surfaceBright = surfaceBright;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceBright(int surfaceBright) {
|
|
||||||
this.surfaceBright = surfaceBright;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceContainerLowest() {
|
|
||||||
return surfaceContainerLowest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceContainerLowest(int surfaceContainerLowest) {
|
|
||||||
this.surfaceContainerLowest = surfaceContainerLowest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceContainerLowest(int surfaceContainerLowest) {
|
|
||||||
this.surfaceContainerLowest = surfaceContainerLowest;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceContainerLow() {
|
|
||||||
return surfaceContainerLow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceContainerLow(int surfaceContainerLow) {
|
|
||||||
this.surfaceContainerLow = surfaceContainerLow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceContainerLow(int surfaceContainerLow) {
|
|
||||||
this.surfaceContainerLow = surfaceContainerLow;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceContainer() {
|
|
||||||
return surfaceContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceContainer(int surfaceContainer) {
|
|
||||||
this.surfaceContainer = surfaceContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceContainer(int surfaceContainer) {
|
|
||||||
this.surfaceContainer = surfaceContainer;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceContainerHigh() {
|
|
||||||
return surfaceContainerHigh;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceContainerHigh(int surfaceContainerHigh) {
|
|
||||||
this.surfaceContainerHigh = surfaceContainerHigh;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceContainerHigh(int surfaceContainerHigh) {
|
|
||||||
this.surfaceContainerHigh = surfaceContainerHigh;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSurfaceContainerHighest() {
|
|
||||||
return surfaceContainerHighest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSurfaceContainerHighest(int surfaceContainerHighest) {
|
|
||||||
this.surfaceContainerHighest = surfaceContainerHighest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scheme withSurfaceContainerHighest(int surfaceContainerHighest) {
|
|
||||||
this.surfaceContainerHighest = surfaceContainerHighest;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Scheme{"
|
|
||||||
+ "primary="
|
|
||||||
+ primary
|
|
||||||
+ ", onPrimary="
|
|
||||||
+ onPrimary
|
|
||||||
+ ", primaryContainer="
|
|
||||||
+ primaryContainer
|
|
||||||
+ ", onPrimaryContainer="
|
|
||||||
+ onPrimaryContainer
|
|
||||||
+ ", secondary="
|
|
||||||
+ secondary
|
|
||||||
+ ", onSecondary="
|
|
||||||
+ onSecondary
|
|
||||||
+ ", secondaryContainer="
|
|
||||||
+ secondaryContainer
|
|
||||||
+ ", onSecondaryContainer="
|
|
||||||
+ onSecondaryContainer
|
|
||||||
+ ", tertiary="
|
|
||||||
+ tertiary
|
|
||||||
+ ", onTertiary="
|
|
||||||
+ onTertiary
|
|
||||||
+ ", tertiaryContainer="
|
|
||||||
+ tertiaryContainer
|
|
||||||
+ ", onTertiaryContainer="
|
|
||||||
+ onTertiaryContainer
|
|
||||||
+ ", error="
|
|
||||||
+ error
|
|
||||||
+ ", onError="
|
|
||||||
+ onError
|
|
||||||
+ ", errorContainer="
|
|
||||||
+ errorContainer
|
|
||||||
+ ", onErrorContainer="
|
|
||||||
+ onErrorContainer
|
|
||||||
+ ", background="
|
|
||||||
+ background
|
|
||||||
+ ", onBackground="
|
|
||||||
+ onBackground
|
|
||||||
+ ", surface="
|
|
||||||
+ surface
|
|
||||||
+ ", onSurface="
|
|
||||||
+ onSurface
|
|
||||||
+ ", surfaceVariant="
|
|
||||||
+ surfaceVariant
|
|
||||||
+ ", onSurfaceVariant="
|
|
||||||
+ onSurfaceVariant
|
|
||||||
+ ", outline="
|
|
||||||
+ outline
|
|
||||||
+ ", outlineVariant="
|
|
||||||
+ outlineVariant
|
|
||||||
+ ", shadow="
|
|
||||||
+ shadow
|
|
||||||
+ ", scrim="
|
|
||||||
+ scrim
|
|
||||||
+ ", inverseSurface="
|
|
||||||
+ inverseSurface
|
|
||||||
+ ", inverseOnSurface="
|
|
||||||
+ inverseOnSurface
|
|
||||||
+ ", inversePrimary="
|
|
||||||
+ inversePrimary
|
|
||||||
+ ", surfaceDim="
|
|
||||||
+ surfaceDim
|
|
||||||
+ ", surfaceBright="
|
|
||||||
+ surfaceBright
|
|
||||||
+ ", surfaceContainerLowest="
|
|
||||||
+ surfaceContainerLowest
|
|
||||||
+ ", surfaceContainerLow="
|
|
||||||
+ surfaceContainerLow
|
|
||||||
+ ", surfaceContainer="
|
|
||||||
+ surfaceContainer
|
|
||||||
+ ", surfaceContainerHigh="
|
|
||||||
+ surfaceContainerHigh
|
|
||||||
+ ", surfaceContainerHighest="
|
|
||||||
+ surfaceContainerHighest
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object object) {
|
|
||||||
if (this == object) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(object instanceof Scheme)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!super.equals(object)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Scheme scheme = (Scheme) object;
|
|
||||||
|
|
||||||
if (primary != scheme.primary) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onPrimary != scheme.onPrimary) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (primaryContainer != scheme.primaryContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onPrimaryContainer != scheme.onPrimaryContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (secondary != scheme.secondary) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onSecondary != scheme.onSecondary) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (secondaryContainer != scheme.secondaryContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onSecondaryContainer != scheme.onSecondaryContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (tertiary != scheme.tertiary) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onTertiary != scheme.onTertiary) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (tertiaryContainer != scheme.tertiaryContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onTertiaryContainer != scheme.onTertiaryContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (error != scheme.error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onError != scheme.onError) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (errorContainer != scheme.errorContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onErrorContainer != scheme.onErrorContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (background != scheme.background) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onBackground != scheme.onBackground) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surface != scheme.surface) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onSurface != scheme.onSurface) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceVariant != scheme.surfaceVariant) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (onSurfaceVariant != scheme.onSurfaceVariant) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (outline != scheme.outline) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (outlineVariant != scheme.outlineVariant) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (shadow != scheme.shadow) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (scrim != scheme.scrim) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (inverseSurface != scheme.inverseSurface) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (inverseOnSurface != scheme.inverseOnSurface) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (inversePrimary != scheme.inversePrimary) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceDim != scheme.surfaceDim) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceBright != scheme.surfaceBright) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceContainerLowest != scheme.surfaceContainerLowest) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceContainerLow != scheme.surfaceContainerLow) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceContainer != scheme.surfaceContainer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceContainerHigh != scheme.surfaceContainerHigh) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (surfaceContainerHighest != scheme.surfaceContainerHighest) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = super.hashCode();
|
|
||||||
result = 31 * result + primary;
|
|
||||||
result = 31 * result + onPrimary;
|
|
||||||
result = 31 * result + primaryContainer;
|
|
||||||
result = 31 * result + onPrimaryContainer;
|
|
||||||
result = 31 * result + secondary;
|
|
||||||
result = 31 * result + onSecondary;
|
|
||||||
result = 31 * result + secondaryContainer;
|
|
||||||
result = 31 * result + onSecondaryContainer;
|
|
||||||
result = 31 * result + tertiary;
|
|
||||||
result = 31 * result + onTertiary;
|
|
||||||
result = 31 * result + tertiaryContainer;
|
|
||||||
result = 31 * result + onTertiaryContainer;
|
|
||||||
result = 31 * result + error;
|
|
||||||
result = 31 * result + onError;
|
|
||||||
result = 31 * result + errorContainer;
|
|
||||||
result = 31 * result + onErrorContainer;
|
|
||||||
result = 31 * result + background;
|
|
||||||
result = 31 * result + onBackground;
|
|
||||||
result = 31 * result + surface;
|
|
||||||
result = 31 * result + onSurface;
|
|
||||||
result = 31 * result + surfaceVariant;
|
|
||||||
result = 31 * result + onSurfaceVariant;
|
|
||||||
result = 31 * result + outline;
|
|
||||||
result = 31 * result + outlineVariant;
|
|
||||||
result = 31 * result + shadow;
|
|
||||||
result = 31 * result + scrim;
|
|
||||||
result = 31 * result + inverseSurface;
|
|
||||||
result = 31 * result + inverseOnSurface;
|
|
||||||
result = 31 * result + inversePrimary;
|
|
||||||
result = 31 * result + surfaceDim;
|
|
||||||
result = 31 * result + surfaceBright;
|
|
||||||
result = 31 * result + surfaceContainerLowest;
|
|
||||||
result = 31 * result + surfaceContainerLow;
|
|
||||||
result = 31 * result + surfaceContainer;
|
|
||||||
result = 31 * result + surfaceContainerHigh;
|
|
||||||
result = 31 * result + surfaceContainerHighest;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A scheme that places the source color in Scheme.primaryContainer.
|
||||||
|
*
|
||||||
|
* Primary Container is the source color, adjusted for color relativity. It maintains constant
|
||||||
|
* appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in
|
||||||
|
* dark mode.
|
||||||
|
*
|
||||||
|
* Tertiary Container is an analogous color, specifically, the analog of a color wheel divided into
|
||||||
|
* 6, and the precise analog is the one found by increasing hue. This is a scientifically grounded
|
||||||
|
* equivalent to rotating hue clockwise by 60 degrees. It also maintains constant appearance.
|
||||||
|
*/
|
||||||
|
class SchemeContent(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.CONTENT,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.CONTENT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.CONTENT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.CONTENT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.CONTENT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(Variant.CONTENT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.CONTENT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/** A playful theme - the source color's hue does not appear in the theme. */
|
||||||
|
class SchemeExpressive(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.EXPRESSIVE,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.EXPRESSIVE, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.EXPRESSIVE, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.EXPRESSIVE, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.EXPRESSIVE, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(
|
||||||
|
Variant.EXPRESSIVE,
|
||||||
|
sourceColorHct,
|
||||||
|
isDark,
|
||||||
|
platform,
|
||||||
|
contrastLevel,
|
||||||
|
),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.EXPRESSIVE, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A scheme that places the source color in Scheme.primaryContainer.
|
||||||
|
*
|
||||||
|
* Primary Container is the source color, adjusted for color relativity. It maintains constant
|
||||||
|
* appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in
|
||||||
|
* dark mode.
|
||||||
|
*
|
||||||
|
* Tertiary Container is the complement to the source color, using TemperatureCache. It also
|
||||||
|
* maintains constant appearance.
|
||||||
|
*/
|
||||||
|
class SchemeFidelity(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.FIDELITY,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.FIDELITY, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.FIDELITY, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.FIDELITY, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.FIDELITY, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(Variant.FIDELITY, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.FIDELITY, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/** A playful theme - the source color's hue does not appear in the theme. */
|
||||||
|
class SchemeFruitSalad(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.FRUIT_SALAD,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.FRUIT_SALAD, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.FRUIT_SALAD, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.FRUIT_SALAD, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.FRUIT_SALAD, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(
|
||||||
|
Variant.FRUIT_SALAD,
|
||||||
|
sourceColorHct,
|
||||||
|
isDark,
|
||||||
|
platform,
|
||||||
|
contrastLevel,
|
||||||
|
),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.FRUIT_SALAD, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/** A monochrome theme, colors are purely black / white / gray. */
|
||||||
|
class SchemeMonochrome(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.MONOCHROME,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.MONOCHROME, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.MONOCHROME, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.MONOCHROME, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.MONOCHROME, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(
|
||||||
|
Variant.MONOCHROME,
|
||||||
|
sourceColorHct,
|
||||||
|
isDark,
|
||||||
|
platform,
|
||||||
|
contrastLevel,
|
||||||
|
),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.MONOCHROME, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/** A theme that's slightly more chromatic than monochrome, which is purely black / white / gray. */
|
||||||
|
class SchemeNeutral(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.NEUTRAL,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.NEUTRAL, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.NEUTRAL, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.NEUTRAL, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.NEUTRAL, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(Variant.NEUTRAL, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.NEUTRAL, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/** A playful theme - the source color's hue does not appear in the theme. */
|
||||||
|
class SchemeRainbow(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.RAINBOW,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.RAINBOW, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.RAINBOW, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.RAINBOW, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.RAINBOW, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(Variant.RAINBOW, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.RAINBOW, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/** A calm theme, sedated colors that aren't particularly chromatic. */
|
||||||
|
class SchemeTonalSpot(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.TONAL_SPOT,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.TONAL_SPOT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.TONAL_SPOT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.TONAL_SPOT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.TONAL_SPOT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(
|
||||||
|
Variant.TONAL_SPOT,
|
||||||
|
sourceColorHct,
|
||||||
|
isDark,
|
||||||
|
platform,
|
||||||
|
contrastLevel,
|
||||||
|
),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.TONAL_SPOT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import dynamiccolor.ColorSpec.SpecVersion
|
||||||
|
import dynamiccolor.ColorSpecs
|
||||||
|
import dynamiccolor.DynamicScheme
|
||||||
|
import dynamiccolor.Variant
|
||||||
|
import hct.Hct
|
||||||
|
|
||||||
|
/** A loud theme, colorfulness is maximum for Primary palette, increased for others. */
|
||||||
|
class SchemeVibrant(
|
||||||
|
sourceColorHct: Hct,
|
||||||
|
isDark: Boolean,
|
||||||
|
contrastLevel: Double,
|
||||||
|
specVersion: SpecVersion = DEFAULT_SPEC_VERSION,
|
||||||
|
platform: Platform = DEFAULT_PLATFORM,
|
||||||
|
) :
|
||||||
|
DynamicScheme(
|
||||||
|
sourceColorHct,
|
||||||
|
Variant.VIBRANT,
|
||||||
|
isDark,
|
||||||
|
contrastLevel,
|
||||||
|
platform,
|
||||||
|
specVersion,
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getPrimaryPalette(Variant.VIBRANT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getSecondaryPalette(Variant.VIBRANT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getTertiaryPalette(Variant.VIBRANT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralPalette(Variant.VIBRANT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getNeutralVariantPalette(Variant.VIBRANT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
ColorSpecs.get(specVersion)
|
||||||
|
.getErrorPalette(Variant.VIBRANT, sourceColorHct, isDark, platform, contrastLevel),
|
||||||
|
)
|
||||||
164
libs/material-color-utilities/src/main/java/score/Score.kt
Normal file
164
libs/material-color-utilities/src/main/java/score/Score.kt
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package score
|
||||||
|
|
||||||
|
import hct.Hct
|
||||||
|
import utils.MathUtils
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.Collections
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest
|
||||||
|
* based on suitability.
|
||||||
|
*
|
||||||
|
* Enables use of a high cluster count for image quantization, thus ensuring colors aren't muddied,
|
||||||
|
* while curating the high cluster count to a much smaller number of appropriate choices.
|
||||||
|
*/
|
||||||
|
object Score {
|
||||||
|
private const val TARGET_CHROMA = 48.0 // A1 Chroma
|
||||||
|
private const val WEIGHT_PROPORTION = 0.7
|
||||||
|
private const val WEIGHT_CHROMA_ABOVE = 0.3
|
||||||
|
private const val WEIGHT_CHROMA_BELOW = 0.1
|
||||||
|
private const val CUTOFF_CHROMA = 5.0
|
||||||
|
private const val CUTOFF_EXCITED_PROPORTION = 0.01
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun score(colorsToPopulation: Map<Int, Int>): List<Int> {
|
||||||
|
// Fallback color is Google Blue.
|
||||||
|
return score(colorsToPopulation, 4, 0xff4285f4.toInt(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun score(colorsToPopulation: Map<Int, Int>, desired: Int): List<Int> {
|
||||||
|
return score(colorsToPopulation, desired, 0xff4285f4.toInt(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun score(colorsToPopulation: Map<Int, Int>, desired: Int, fallbackColorArgb: Int): List<Int> {
|
||||||
|
return score(colorsToPopulation, desired, fallbackColorArgb, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a map with keys of colors and values of how often the color appears, rank the colors
|
||||||
|
* based on suitability for being used for a UI theme.
|
||||||
|
*
|
||||||
|
* @param colorsToPopulation map with keys of colors and values of how often the color appears,
|
||||||
|
* usually from a source image.
|
||||||
|
* @param desired max count of colors to be returned in the list.
|
||||||
|
* @param fallbackColorArgb color to be returned if no other options available.
|
||||||
|
* @param filter whether to filter out undesireable combinations.
|
||||||
|
* @return Colors sorted by suitability for a UI theme. The most suitable color is the first item,
|
||||||
|
* the least suitable is the last. There will always be at least one color returned. If all the
|
||||||
|
* input colors were not suitable for a theme, a default fallback color will be provided, Google
|
||||||
|
* Blue.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun score(
|
||||||
|
colorsToPopulation: Map<Int, Int>,
|
||||||
|
desired: Int,
|
||||||
|
fallbackColorArgb: Int,
|
||||||
|
filter: Boolean,
|
||||||
|
): List<Int> {
|
||||||
|
|
||||||
|
// Get the HCT color for each Argb value, while finding the per hue count and
|
||||||
|
// total count.
|
||||||
|
val colorsHct: MutableList<Hct> = ArrayList()
|
||||||
|
val huePopulation = IntArray(360)
|
||||||
|
var populationSum = 0.0
|
||||||
|
for ((key, value) in colorsToPopulation) {
|
||||||
|
val hct = Hct.fromInt(key)
|
||||||
|
colorsHct.add(hct)
|
||||||
|
val hue = floor(hct.hue).toInt()
|
||||||
|
huePopulation[hue] += value
|
||||||
|
populationSum += value.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hues with more usage in neighboring 30 degree slice get a larger number.
|
||||||
|
val hueExcitedProportions = DoubleArray(360)
|
||||||
|
for (hue in 0..359) {
|
||||||
|
val proportion = huePopulation[hue] / populationSum
|
||||||
|
for (i in hue - 14 until hue + 16) {
|
||||||
|
val neighborHue = MathUtils.sanitizeDegreesInt(i)
|
||||||
|
hueExcitedProportions[neighborHue] += proportion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scores each HCT color based on usage and chroma, while optionally
|
||||||
|
// filtering out values that do not have enough chroma or usage.
|
||||||
|
val scoredHcts: MutableList<ScoredHCT> = ArrayList()
|
||||||
|
for (hct in colorsHct) {
|
||||||
|
val hue = MathUtils.sanitizeDegreesInt(hct.hue.roundToInt())
|
||||||
|
val proportion = hueExcitedProportions[hue]
|
||||||
|
if (filter && (hct.chroma < CUTOFF_CHROMA || proportion <= CUTOFF_EXCITED_PROPORTION)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val proportionScore = proportion * 100.0 * WEIGHT_PROPORTION
|
||||||
|
val chromaWeight =
|
||||||
|
if (hct.chroma < TARGET_CHROMA) WEIGHT_CHROMA_BELOW else WEIGHT_CHROMA_ABOVE
|
||||||
|
val chromaScore = (hct.chroma - TARGET_CHROMA) * chromaWeight
|
||||||
|
val score = proportionScore + chromaScore
|
||||||
|
scoredHcts.add(ScoredHCT(hct, score))
|
||||||
|
}
|
||||||
|
// Sorted so that colors with higher scores come first.
|
||||||
|
Collections.sort(scoredHcts, ScoredComparator())
|
||||||
|
|
||||||
|
// Iterates through potential hue differences in degrees in order to select
|
||||||
|
// the colors with the largest distribution of hues possible. Starting at
|
||||||
|
// 90 degrees(maximum difference for 4 colors) then decreasing down to a
|
||||||
|
// 15 degree minimum.
|
||||||
|
val chosenColors: MutableList<Hct> = ArrayList()
|
||||||
|
for (differenceDegrees in 90 downTo 15) {
|
||||||
|
chosenColors.clear()
|
||||||
|
for (entry in scoredHcts) {
|
||||||
|
val hct = entry.hct
|
||||||
|
var hasDuplicateHue = false
|
||||||
|
for (chosenHct in chosenColors) {
|
||||||
|
if (MathUtils.differenceDegrees(hct.hue, chosenHct.hue) < differenceDegrees) {
|
||||||
|
hasDuplicateHue = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasDuplicateHue) {
|
||||||
|
chosenColors.add(hct)
|
||||||
|
}
|
||||||
|
if (chosenColors.size >= desired) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chosenColors.size >= desired) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val colors: MutableList<Int> = ArrayList()
|
||||||
|
if (chosenColors.isEmpty()) {
|
||||||
|
colors.add(fallbackColorArgb)
|
||||||
|
}
|
||||||
|
for (chosenHct in chosenColors) {
|
||||||
|
colors.add(chosenHct.toInt())
|
||||||
|
}
|
||||||
|
return colors
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScoredHCT(val hct: Hct, val score: Double)
|
||||||
|
|
||||||
|
private class ScoredComparator() : Comparator<ScoredHCT> {
|
||||||
|
override fun compare(entry1: ScoredHCT, entry2: ScoredHCT): Int {
|
||||||
|
return entry2.score.compareTo(entry1.score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package temperature
|
||||||
|
|
||||||
|
import hct.Hct
|
||||||
|
import utils.ColorUtils
|
||||||
|
import utils.MathUtils
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.HashMap
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.hypot
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design utilities using color temperature theory.
|
||||||
|
*
|
||||||
|
* Analogous colors, complementary color, and cache to efficiently, lazily, generate data for
|
||||||
|
* calculations when needed.
|
||||||
|
*/
|
||||||
|
class TemperatureCache(val input: Hct) {
|
||||||
|
private var _precomputedComplement: Hct? = null
|
||||||
|
private var _precomputedHctsByTemp: List<Hct>? = null
|
||||||
|
private var _precomputedHctsByHue: List<Hct>? = null
|
||||||
|
private var _precomputedTempsByHct: Map<Hct, Double>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A color that complements the input color aesthetically.
|
||||||
|
*
|
||||||
|
* In art, this is usually described as being across the color wheel. History of this shows intent
|
||||||
|
* as a color that is just as cool-warm as the input color is warm-cool.
|
||||||
|
*/
|
||||||
|
val complement: Hct
|
||||||
|
get() {
|
||||||
|
if (_precomputedComplement != null) {
|
||||||
|
return _precomputedComplement!!
|
||||||
|
}
|
||||||
|
val coldestHue = coldest.hue
|
||||||
|
val coldestTemp = tempsByHct[coldest]!!
|
||||||
|
val warmestHue = warmest.hue
|
||||||
|
val warmestTemp = tempsByHct[warmest]!!
|
||||||
|
val range = warmestTemp - coldestTemp
|
||||||
|
val startHueIsColdestToWarmest = isBetween(input.hue, coldestHue, warmestHue)
|
||||||
|
val startHue = if (startHueIsColdestToWarmest) warmestHue else coldestHue
|
||||||
|
val endHue = if (startHueIsColdestToWarmest) coldestHue else warmestHue
|
||||||
|
val directionOfRotation = 1.0
|
||||||
|
var smallestError = 1000.0
|
||||||
|
var answer = hctsByHue[input.hue.roundToInt()]
|
||||||
|
val complementRelativeTemp = 1.0 - getRelativeTemperature(input)
|
||||||
|
// Find the color in the other section, closest to the inverse percentile
|
||||||
|
// of the input color. This is the complement.
|
||||||
|
var hueAddend = 0.0
|
||||||
|
while (hueAddend <= 360.0) {
|
||||||
|
val hue = MathUtils.sanitizeDegreesDouble(startHue + directionOfRotation * hueAddend)
|
||||||
|
if (!isBetween(hue, startHue, endHue)) {
|
||||||
|
hueAddend += 1.0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val possibleAnswer = hctsByHue[hue.roundToInt()]
|
||||||
|
val relativeTemp = (tempsByHct[possibleAnswer]!! - coldestTemp) / range
|
||||||
|
val error = abs(complementRelativeTemp - relativeTemp)
|
||||||
|
if (error < smallestError) {
|
||||||
|
smallestError = error
|
||||||
|
answer = possibleAnswer
|
||||||
|
}
|
||||||
|
hueAddend += 1.0
|
||||||
|
}
|
||||||
|
_precomputedComplement = answer
|
||||||
|
return _precomputedComplement!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5 colors that pair well with the input color.
|
||||||
|
*
|
||||||
|
* The colors are equidistant in temperature and adjacent in hue.
|
||||||
|
*/
|
||||||
|
fun getAnalogousColors(): List<Hct> {
|
||||||
|
return getAnalogousColors(5, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of colors with differing hues, equidistant in temperature.
|
||||||
|
*
|
||||||
|
* In art, this is usually described as a set of 5 colors on a color wheel divided into 12
|
||||||
|
* sections. This method allows provision of either of those values.
|
||||||
|
*
|
||||||
|
* Behavior is undefined when count or divisions is 0. When divisions < count, colors repeat.
|
||||||
|
*
|
||||||
|
* @param count The number of colors to return, includes the input color.
|
||||||
|
* @param divisions The number of divisions on the color wheel.
|
||||||
|
*/
|
||||||
|
fun getAnalogousColors(count: Int, divisions: Int): List<Hct> {
|
||||||
|
// The starting hue is the hue of the input color.
|
||||||
|
val startHue = input.hue.roundToInt()
|
||||||
|
val startHct = hctsByHue[startHue]
|
||||||
|
var lastTemp = getRelativeTemperature(startHct)
|
||||||
|
val allColors: MutableList<Hct> = ArrayList()
|
||||||
|
allColors.add(startHct)
|
||||||
|
var absoluteTotalTempDelta = 0f
|
||||||
|
for (i in 0..359) {
|
||||||
|
val hue = MathUtils.sanitizeDegreesInt(startHue + i)
|
||||||
|
val hct = hctsByHue[hue]
|
||||||
|
val temp = getRelativeTemperature(hct)
|
||||||
|
val tempDelta = abs(temp - lastTemp)
|
||||||
|
lastTemp = temp
|
||||||
|
absoluteTotalTempDelta += tempDelta.toFloat()
|
||||||
|
}
|
||||||
|
var hueAddend = 1
|
||||||
|
val tempStep = absoluteTotalTempDelta / divisions.toDouble()
|
||||||
|
var totalTempDelta = 0.0
|
||||||
|
lastTemp = getRelativeTemperature(startHct)
|
||||||
|
while (allColors.size < divisions) {
|
||||||
|
val hue = MathUtils.sanitizeDegreesInt(startHue + hueAddend)
|
||||||
|
val hct = hctsByHue[hue]
|
||||||
|
val temp = getRelativeTemperature(hct)
|
||||||
|
val tempDelta = abs(temp - lastTemp)
|
||||||
|
totalTempDelta += tempDelta
|
||||||
|
var desiredTotalTempDeltaForIndex = allColors.size * tempStep
|
||||||
|
var indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex
|
||||||
|
var indexAddend = 1
|
||||||
|
// Keep adding this hue to the answers until its temperature is
|
||||||
|
// insufficient. This ensures consistent behavior when there aren't
|
||||||
|
// `divisions` discrete steps between 0 and 360 in hue with `tempStep`
|
||||||
|
// delta in temperature between them.
|
||||||
|
//
|
||||||
|
// For example, white and black have no analogues: there are no other
|
||||||
|
// colors at T100/T0. Therefore, they should just be added to the array
|
||||||
|
// as answers.
|
||||||
|
while (indexSatisfied && allColors.size < divisions) {
|
||||||
|
allColors.add(hct)
|
||||||
|
desiredTotalTempDeltaForIndex = (allColors.size + indexAddend) * tempStep
|
||||||
|
indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex
|
||||||
|
indexAddend++
|
||||||
|
}
|
||||||
|
lastTemp = temp
|
||||||
|
hueAddend++
|
||||||
|
if (hueAddend > 360) {
|
||||||
|
while (allColors.size < divisions) {
|
||||||
|
allColors.add(hct)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val answers: MutableList<Hct> = ArrayList()
|
||||||
|
answers.add(input)
|
||||||
|
val ccwCount = floor(((count - 1.0) / 2.0)).toInt()
|
||||||
|
for (i in 1 until ccwCount + 1) {
|
||||||
|
var index = 0 - i
|
||||||
|
while (index < 0) {
|
||||||
|
index = allColors.size + index
|
||||||
|
}
|
||||||
|
if (index >= allColors.size) {
|
||||||
|
index %= allColors.size
|
||||||
|
}
|
||||||
|
answers.add(0, allColors[index])
|
||||||
|
}
|
||||||
|
val cwCount = count - ccwCount - 1
|
||||||
|
for (i in 1 until cwCount + 1) {
|
||||||
|
var index = i
|
||||||
|
while (index < 0) {
|
||||||
|
index = allColors.size + index
|
||||||
|
}
|
||||||
|
if (index >= allColors.size) {
|
||||||
|
index %= allColors.size
|
||||||
|
}
|
||||||
|
answers.add(allColors[index])
|
||||||
|
}
|
||||||
|
return answers
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temperature relative to all colors with the same chroma and tone.
|
||||||
|
*
|
||||||
|
* @param hct HCT to find the relative temperature of.
|
||||||
|
* @return Value on a scale from 0 to 1.
|
||||||
|
*/
|
||||||
|
fun getRelativeTemperature(hct: Hct): Double {
|
||||||
|
val range = tempsByHct[warmest]!! - tempsByHct[coldest]!!
|
||||||
|
val differenceFromColdest = tempsByHct[hct]!! - tempsByHct[coldest]!!
|
||||||
|
// Handle when there's no difference in temperature between warmest and
|
||||||
|
// coldest: for example, at T100, only one color is available, white.
|
||||||
|
return if (range == 0.0) {
|
||||||
|
0.5
|
||||||
|
} else {
|
||||||
|
differenceFromColdest / range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Coldest color with same chroma and tone as input. */
|
||||||
|
private val coldest: Hct
|
||||||
|
get() = hctsByTemp[0]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HCTs for all colors with the same chroma/tone as the input.
|
||||||
|
*
|
||||||
|
* Sorted by hue, ex. index 0 is hue 0.
|
||||||
|
*/
|
||||||
|
private val hctsByHue: List<Hct>
|
||||||
|
get() {
|
||||||
|
if (_precomputedHctsByHue != null) {
|
||||||
|
return _precomputedHctsByHue!!
|
||||||
|
}
|
||||||
|
val hcts: MutableList<Hct> = ArrayList()
|
||||||
|
var hue = 0.0
|
||||||
|
while (hue <= 360.0) {
|
||||||
|
val colorAtHue = Hct.from(hue, input.chroma, input.tone)
|
||||||
|
hcts.add(colorAtHue)
|
||||||
|
hue += 1.0
|
||||||
|
}
|
||||||
|
val unmodifiableList = Collections.unmodifiableList(hcts)
|
||||||
|
_precomputedHctsByHue = unmodifiableList
|
||||||
|
return unmodifiableList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HCTs for all colors with the same chroma/tone as the input.
|
||||||
|
*
|
||||||
|
* Sorted from coldest first to warmest last.
|
||||||
|
*/
|
||||||
|
private val hctsByTemp: List<Hct>
|
||||||
|
get() {
|
||||||
|
if (_precomputedHctsByTemp != null) {
|
||||||
|
return _precomputedHctsByTemp!!
|
||||||
|
}
|
||||||
|
val hcts: MutableList<Hct> = ArrayList(hctsByHue)
|
||||||
|
hcts.add(input)
|
||||||
|
hcts.sortWith(compareBy { tempsByHct[it]!! })
|
||||||
|
_precomputedHctsByTemp = hcts
|
||||||
|
return hcts
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keys of HCTs in getHctsByTemp, values of raw temperature. */
|
||||||
|
private val tempsByHct: Map<Hct, Double>
|
||||||
|
get() {
|
||||||
|
if (_precomputedTempsByHct != null) {
|
||||||
|
return _precomputedTempsByHct!!
|
||||||
|
}
|
||||||
|
val allHcts: MutableList<Hct> = ArrayList(hctsByHue)
|
||||||
|
allHcts.add(input)
|
||||||
|
val temperaturesByHct: MutableMap<Hct, Double> = HashMap()
|
||||||
|
for (hct in allHcts) {
|
||||||
|
temperaturesByHct[hct] = rawTemperature(hct)
|
||||||
|
}
|
||||||
|
_precomputedTempsByHct = temperaturesByHct
|
||||||
|
return temperaturesByHct
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Warmest color with same chroma and tone as input. */
|
||||||
|
private val warmest: Hct
|
||||||
|
get() = hctsByTemp[hctsByTemp.size - 1]
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Value representing cool-warm factor of a color. Values below 0 are considered cool, above,
|
||||||
|
* warm.
|
||||||
|
*
|
||||||
|
* Color science has researched emotion and harmony, which art uses to select colors. Warm-cool
|
||||||
|
* is the foundation of analogous and complementary colors. See:
|
||||||
|
* - Li-Chen Ou's Chapter 19 in Handbook of Color Psychology (2015).
|
||||||
|
* - Josef Albers' Interaction of Color chapters 19 and 21.
|
||||||
|
*
|
||||||
|
* Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space. Return
|
||||||
|
* value has these properties:
|
||||||
|
* - Values below 0 are cool, above 0 are warm.
|
||||||
|
* - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.
|
||||||
|
* - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130.
|
||||||
|
*/
|
||||||
|
fun rawTemperature(color: Hct): Double {
|
||||||
|
val lab = ColorUtils.labFromArgb(color.toInt())
|
||||||
|
val hue = MathUtils.sanitizeDegreesDouble(Math.toDegrees(atan2(lab[2], lab[1])))
|
||||||
|
val chroma = hypot(lab[1], lab[2])
|
||||||
|
return -0.5 +
|
||||||
|
0.02 * chroma.pow(1.07) * cos(Math.toRadians(MathUtils.sanitizeDegreesDouble(hue - 50.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determines if an angle is between two other angles, rotating clockwise. */
|
||||||
|
private fun isBetween(angle: Double, a: Double, b: Double): Boolean {
|
||||||
|
return if (a < b) {
|
||||||
|
a <= angle && angle <= b
|
||||||
|
} else {
|
||||||
|
a <= angle || angle <= b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This file is automatically generated. Do not modify it.
|
|
||||||
|
|
||||||
package utils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Color science utilities.
|
|
||||||
*
|
|
||||||
* <p>Utility methods for color science constants and color space conversions that aren't HCT or
|
|
||||||
* CAM16.
|
|
||||||
*/
|
|
||||||
public class ColorUtils {
|
|
||||||
private ColorUtils() {}
|
|
||||||
|
|
||||||
static final double[][] SRGB_TO_XYZ =
|
|
||||||
new double[][] {
|
|
||||||
new double[] {0.41233895, 0.35762064, 0.18051042},
|
|
||||||
new double[] {0.2126, 0.7152, 0.0722},
|
|
||||||
new double[] {0.01932141, 0.11916382, 0.95034478},
|
|
||||||
};
|
|
||||||
|
|
||||||
static final double[][] XYZ_TO_SRGB =
|
|
||||||
new double[][] {
|
|
||||||
new double[] {
|
|
||||||
3.2413774792388685, -1.5376652402851851, -0.49885366846268053,
|
|
||||||
},
|
|
||||||
new double[] {
|
|
||||||
-0.9691452513005321, 1.8758853451067872, 0.04156585616912061,
|
|
||||||
},
|
|
||||||
new double[] {
|
|
||||||
0.05562093689691305, -0.20395524564742123, 1.0571799111220335,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static final double[] WHITE_POINT_D65 = new double[] {95.047, 100.0, 108.883};
|
|
||||||
|
|
||||||
/** Converts a color from RGB components to ARGB format. */
|
|
||||||
public static int argbFromRgb(int red, int green, int blue) {
|
|
||||||
return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts a color from linear RGB components to ARGB format. */
|
|
||||||
public static int argbFromLinrgb(double[] linrgb) {
|
|
||||||
int r = delinearized(linrgb[0]);
|
|
||||||
int g = delinearized(linrgb[1]);
|
|
||||||
int b = delinearized(linrgb[2]);
|
|
||||||
return argbFromRgb(r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the alpha component of a color in ARGB format. */
|
|
||||||
public static int alphaFromArgb(int argb) {
|
|
||||||
return (argb >> 24) & 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the red component of a color in ARGB format. */
|
|
||||||
public static int redFromArgb(int argb) {
|
|
||||||
return (argb >> 16) & 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the green component of a color in ARGB format. */
|
|
||||||
public static int greenFromArgb(int argb) {
|
|
||||||
return (argb >> 8) & 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the blue component of a color in ARGB format. */
|
|
||||||
public static int blueFromArgb(int argb) {
|
|
||||||
return argb & 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns whether a color in ARGB format is opaque. */
|
|
||||||
public static boolean isOpaque(int argb) {
|
|
||||||
return alphaFromArgb(argb) >= 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts a color from ARGB to XYZ. */
|
|
||||||
public static int argbFromXyz(double x, double y, double z) {
|
|
||||||
double[][] matrix = XYZ_TO_SRGB;
|
|
||||||
double linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z;
|
|
||||||
double linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z;
|
|
||||||
double linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z;
|
|
||||||
int r = delinearized(linearR);
|
|
||||||
int g = delinearized(linearG);
|
|
||||||
int b = delinearized(linearB);
|
|
||||||
return argbFromRgb(r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts a color from XYZ to ARGB. */
|
|
||||||
public static double[] xyzFromArgb(int argb) {
|
|
||||||
double r = linearized(redFromArgb(argb));
|
|
||||||
double g = linearized(greenFromArgb(argb));
|
|
||||||
double b = linearized(blueFromArgb(argb));
|
|
||||||
return MathUtils.matrixMultiply(new double[] {r, g, b}, SRGB_TO_XYZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts a color represented in Lab color space into an ARGB integer. */
|
|
||||||
public static int argbFromLab(double l, double a, double b) {
|
|
||||||
double[] whitePoint = WHITE_POINT_D65;
|
|
||||||
double fy = (l + 16.0) / 116.0;
|
|
||||||
double fx = a / 500.0 + fy;
|
|
||||||
double fz = fy - b / 200.0;
|
|
||||||
double xNormalized = labInvf(fx);
|
|
||||||
double yNormalized = labInvf(fy);
|
|
||||||
double zNormalized = labInvf(fz);
|
|
||||||
double x = xNormalized * whitePoint[0];
|
|
||||||
double y = yNormalized * whitePoint[1];
|
|
||||||
double z = zNormalized * whitePoint[2];
|
|
||||||
return argbFromXyz(x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a color from ARGB representation to L*a*b* representation.
|
|
||||||
*
|
|
||||||
* @param argb the ARGB representation of a color
|
|
||||||
* @return a Lab object representing the color
|
|
||||||
*/
|
|
||||||
public static double[] labFromArgb(int argb) {
|
|
||||||
double linearR = linearized(redFromArgb(argb));
|
|
||||||
double linearG = linearized(greenFromArgb(argb));
|
|
||||||
double linearB = linearized(blueFromArgb(argb));
|
|
||||||
double[][] matrix = SRGB_TO_XYZ;
|
|
||||||
double x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB;
|
|
||||||
double y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB;
|
|
||||||
double z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB;
|
|
||||||
double[] whitePoint = WHITE_POINT_D65;
|
|
||||||
double xNormalized = x / whitePoint[0];
|
|
||||||
double yNormalized = y / whitePoint[1];
|
|
||||||
double zNormalized = z / whitePoint[2];
|
|
||||||
double fx = labF(xNormalized);
|
|
||||||
double fy = labF(yNormalized);
|
|
||||||
double fz = labF(zNormalized);
|
|
||||||
double l = 116.0 * fy - 16;
|
|
||||||
double a = 500.0 * (fx - fy);
|
|
||||||
double b = 200.0 * (fy - fz);
|
|
||||||
return new double[] {l, a, b};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an L* value to an ARGB representation.
|
|
||||||
*
|
|
||||||
* @param lstar L* in L*a*b*
|
|
||||||
* @return ARGB representation of grayscale color with lightness matching L*
|
|
||||||
*/
|
|
||||||
public static int argbFromLstar(double lstar) {
|
|
||||||
double y = yFromLstar(lstar);
|
|
||||||
int component = delinearized(y);
|
|
||||||
return argbFromRgb(component, component, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the L* value of a color in ARGB representation.
|
|
||||||
*
|
|
||||||
* @param argb ARGB representation of a color
|
|
||||||
* @return L*, from L*a*b*, coordinate of the color
|
|
||||||
*/
|
|
||||||
public static double lstarFromArgb(int argb) {
|
|
||||||
double y = xyzFromArgb(argb)[1];
|
|
||||||
return 116.0 * labF(y / 100.0) - 16.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an L* value to a Y value.
|
|
||||||
*
|
|
||||||
* <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
|
|
||||||
*
|
|
||||||
* <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
|
|
||||||
* logarithmic scale.
|
|
||||||
*
|
|
||||||
* @param lstar L* in L*a*b*
|
|
||||||
* @return Y in XYZ
|
|
||||||
*/
|
|
||||||
public static double yFromLstar(double lstar) {
|
|
||||||
return 100.0 * labInvf((lstar + 16.0) / 116.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a Y value to an L* value.
|
|
||||||
*
|
|
||||||
* <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
|
|
||||||
*
|
|
||||||
* <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
|
|
||||||
* logarithmic scale.
|
|
||||||
*
|
|
||||||
* @param y Y in XYZ
|
|
||||||
* @return L* in L*a*b*
|
|
||||||
*/
|
|
||||||
public static double lstarFromY(double y) {
|
|
||||||
return labF(y / 100.0) * 116.0 - 16.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Linearizes an RGB component.
|
|
||||||
*
|
|
||||||
* @param rgbComponent 0 <= rgb_component <= 255, represents R/G/B channel
|
|
||||||
* @return 0.0 <= output <= 100.0, color channel converted to linear RGB space
|
|
||||||
*/
|
|
||||||
public static double linearized(int rgbComponent) {
|
|
||||||
double normalized = rgbComponent / 255.0;
|
|
||||||
if (normalized <= 0.040449936) {
|
|
||||||
return normalized / 12.92 * 100.0;
|
|
||||||
} else {
|
|
||||||
return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delinearizes an RGB component.
|
|
||||||
*
|
|
||||||
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
|
|
||||||
* @return 0 <= output <= 255, color channel converted to regular RGB space
|
|
||||||
*/
|
|
||||||
public static int delinearized(double rgbComponent) {
|
|
||||||
double normalized = rgbComponent / 100.0;
|
|
||||||
double delinearized = 0.0;
|
|
||||||
if (normalized <= 0.0031308) {
|
|
||||||
delinearized = normalized * 12.92;
|
|
||||||
} else {
|
|
||||||
delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
|
|
||||||
}
|
|
||||||
return MathUtils.clampInt(0, 255, (int) Math.round(delinearized * 255.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the standard white point; white on a sunny day.
|
|
||||||
*
|
|
||||||
* @return The white point
|
|
||||||
*/
|
|
||||||
public static double[] whitePointD65() {
|
|
||||||
return WHITE_POINT_D65;
|
|
||||||
}
|
|
||||||
|
|
||||||
static double labF(double t) {
|
|
||||||
double e = 216.0 / 24389.0;
|
|
||||||
double kappa = 24389.0 / 27.0;
|
|
||||||
if (t > e) {
|
|
||||||
return Math.pow(t, 1.0 / 3.0);
|
|
||||||
} else {
|
|
||||||
return (kappa * t + 16) / 116;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static double labInvf(double ft) {
|
|
||||||
double e = 216.0 / 24389.0;
|
|
||||||
double kappa = 24389.0 / 27.0;
|
|
||||||
double ft3 = ft * ft * ft;
|
|
||||||
if (ft3 > e) {
|
|
||||||
return ft3;
|
|
||||||
} else {
|
|
||||||
return (116 * ft - 16) / kappa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
256
libs/material-color-utilities/src/main/java/utils/ColorUtils.kt
Normal file
256
libs/material-color-utilities/src/main/java/utils/ColorUtils.kt
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
// This file is automatically generated. Do not modify it.
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color science utilities.
|
||||||
|
*
|
||||||
|
* Utility methods for color science constants and color space conversions that aren't HCT or CAM16.
|
||||||
|
*/
|
||||||
|
object ColorUtils {
|
||||||
|
private val SRGB_TO_XYZ =
|
||||||
|
arrayOf(
|
||||||
|
doubleArrayOf(0.41233895, 0.35762064, 0.18051042),
|
||||||
|
doubleArrayOf(0.2126, 0.7152, 0.0722),
|
||||||
|
doubleArrayOf(0.01932141, 0.11916382, 0.95034478),
|
||||||
|
)
|
||||||
|
private val XYZ_TO_SRGB =
|
||||||
|
arrayOf(
|
||||||
|
doubleArrayOf(3.2413774792388685, -1.5376652402851851, -0.49885366846268053),
|
||||||
|
doubleArrayOf(-0.9691452513005321, 1.8758853451067872, 0.04156585616912061),
|
||||||
|
doubleArrayOf(0.05562093689691305, -0.20395524564742123, 1.0571799111220335),
|
||||||
|
)
|
||||||
|
private val WHITE_POINT_D65 = doubleArrayOf(95.047, 100.0, 108.883)
|
||||||
|
|
||||||
|
/** Converts a color from RGB components to ARGB format. */
|
||||||
|
fun argbFromRgb(red: Int, green: Int, blue: Int): Int {
|
||||||
|
return 255 shl 24 or (red and 255 shl 16) or (green and 255 shl 8) or (blue and 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts a color from linear RGB components to ARGB format. */
|
||||||
|
fun argbFromLinrgb(linrgb: DoubleArray): Int {
|
||||||
|
val r = delinearized(linrgb[0])
|
||||||
|
val g = delinearized(linrgb[1])
|
||||||
|
val b = delinearized(linrgb[2])
|
||||||
|
return argbFromRgb(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the alpha component of a color in ARGB format. */
|
||||||
|
fun alphaFromArgb(argb: Int): Int {
|
||||||
|
return argb shr 24 and 255
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the red component of a color in ARGB format. */
|
||||||
|
fun redFromArgb(argb: Int): Int {
|
||||||
|
return argb shr 16 and 255
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the green component of a color in ARGB format. */
|
||||||
|
fun greenFromArgb(argb: Int): Int {
|
||||||
|
return argb shr 8 and 255
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the blue component of a color in ARGB format. */
|
||||||
|
fun blueFromArgb(argb: Int): Int {
|
||||||
|
return argb and 255
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether a color in ARGB format is opaque. */
|
||||||
|
fun isOpaque(argb: Int): Boolean {
|
||||||
|
return alphaFromArgb(argb) >= 255
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts a color from ARGB to XYZ. */
|
||||||
|
fun argbFromXyz(x: Double, y: Double, z: Double): Int {
|
||||||
|
val matrix = XYZ_TO_SRGB
|
||||||
|
val linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z
|
||||||
|
val linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z
|
||||||
|
val linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z
|
||||||
|
val r = delinearized(linearR)
|
||||||
|
val g = delinearized(linearG)
|
||||||
|
val b = delinearized(linearB)
|
||||||
|
return argbFromRgb(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts a color from XYZ to ARGB. */
|
||||||
|
fun xyzFromArgb(argb: Int): DoubleArray {
|
||||||
|
val r = linearized(redFromArgb(argb))
|
||||||
|
val g = linearized(greenFromArgb(argb))
|
||||||
|
val b = linearized(blueFromArgb(argb))
|
||||||
|
return MathUtils.matrixMultiply(doubleArrayOf(r, g, b), SRGB_TO_XYZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts a color represented in Lab color space into an ARGB integer. */
|
||||||
|
fun argbFromLab(l: Double, a: Double, b: Double): Int {
|
||||||
|
val whitePoint = WHITE_POINT_D65
|
||||||
|
val fy = (l + 16.0) / 116.0
|
||||||
|
val fx = a / 500.0 + fy
|
||||||
|
val fz = fy - b / 200.0
|
||||||
|
val xNormalized = labInvf(fx)
|
||||||
|
val yNormalized = labInvf(fy)
|
||||||
|
val zNormalized = labInvf(fz)
|
||||||
|
val x = xNormalized * whitePoint[0]
|
||||||
|
val y = yNormalized * whitePoint[1]
|
||||||
|
val z = zNormalized * whitePoint[2]
|
||||||
|
return argbFromXyz(x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a color from ARGB representation to L*a*b* representation.
|
||||||
|
*
|
||||||
|
* @param argb the ARGB representation of a color
|
||||||
|
* @return a Lab object representing the color
|
||||||
|
*/
|
||||||
|
fun labFromArgb(argb: Int): DoubleArray {
|
||||||
|
val linearR = linearized(redFromArgb(argb))
|
||||||
|
val linearG = linearized(greenFromArgb(argb))
|
||||||
|
val linearB = linearized(blueFromArgb(argb))
|
||||||
|
val matrix = SRGB_TO_XYZ
|
||||||
|
val x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB
|
||||||
|
val y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB
|
||||||
|
val z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB
|
||||||
|
val whitePoint = WHITE_POINT_D65
|
||||||
|
val xNormalized = x / whitePoint[0]
|
||||||
|
val yNormalized = y / whitePoint[1]
|
||||||
|
val zNormalized = z / whitePoint[2]
|
||||||
|
val fx = labF(xNormalized)
|
||||||
|
val fy = labF(yNormalized)
|
||||||
|
val fz = labF(zNormalized)
|
||||||
|
val l = 116.0 * fy - 16
|
||||||
|
val a = 500.0 * (fx - fy)
|
||||||
|
val b = 200.0 * (fy - fz)
|
||||||
|
return doubleArrayOf(l, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an L* value to an ARGB representation.
|
||||||
|
*
|
||||||
|
* @param lstar L* in L*a*b*
|
||||||
|
* @return ARGB representation of grayscale color with lightness matching L*
|
||||||
|
*/
|
||||||
|
fun argbFromLstar(lstar: Double): Int {
|
||||||
|
val y = yFromLstar(lstar)
|
||||||
|
val component = delinearized(y)
|
||||||
|
return argbFromRgb(component, component, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the L* value of a color in ARGB representation.
|
||||||
|
*
|
||||||
|
* @param argb ARGB representation of a color
|
||||||
|
* @return L*, from L*a*b*, coordinate of the color
|
||||||
|
*/
|
||||||
|
fun lstarFromArgb(argb: Int): Double {
|
||||||
|
val y = xyzFromArgb(argb)[1]
|
||||||
|
return 116.0 * labF(y / 100.0) - 16.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an L* value to a Y value.
|
||||||
|
*
|
||||||
|
* L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
|
||||||
|
*
|
||||||
|
* L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
|
||||||
|
* logarithmic scale.
|
||||||
|
*
|
||||||
|
* @param lstar L* in L*a*b*
|
||||||
|
* @return Y in XYZ
|
||||||
|
*/
|
||||||
|
fun yFromLstar(lstar: Double): Double {
|
||||||
|
return 100.0 * labInvf((lstar + 16.0) / 116.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Y value to an L* value.
|
||||||
|
*
|
||||||
|
* L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
|
||||||
|
*
|
||||||
|
* L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
|
||||||
|
* logarithmic scale.
|
||||||
|
*
|
||||||
|
* @param y Y in XYZ
|
||||||
|
* @return L* in L*a*b*
|
||||||
|
*/
|
||||||
|
fun lstarFromY(y: Double): Double {
|
||||||
|
return labF(y / 100.0) * 116.0 - 16.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linearizes an RGB component.
|
||||||
|
*
|
||||||
|
* @param rgbComponent 0 <= rgb_component <= 255, represents R/G/B channel
|
||||||
|
* @return 0.0 <= output <= 100.0, color channel converted to linear RGB space
|
||||||
|
*/
|
||||||
|
fun linearized(rgbComponent: Int): Double {
|
||||||
|
val normalized = rgbComponent / 255.0
|
||||||
|
return if (normalized <= 0.040449936) {
|
||||||
|
normalized / 12.92 * 100.0
|
||||||
|
} else {
|
||||||
|
((normalized + 0.055) / 1.055).pow(2.4) * 100.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delinearizes an RGB component.
|
||||||
|
*
|
||||||
|
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
|
||||||
|
* @return 0 <= output <= 255, color channel converted to regular RGB space
|
||||||
|
*/
|
||||||
|
fun delinearized(rgbComponent: Double): Int {
|
||||||
|
val normalized = rgbComponent / 100.0
|
||||||
|
val delinearized: Double =
|
||||||
|
if (normalized <= 0.0031308) {
|
||||||
|
normalized * 12.92
|
||||||
|
} else {
|
||||||
|
1.055 * normalized.pow(1.0 / 2.4) - 0.055
|
||||||
|
}
|
||||||
|
return (delinearized * 255.0).roundToInt().coerceIn(0, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the standard white point; white on a sunny day.
|
||||||
|
*
|
||||||
|
* @return The white point
|
||||||
|
*/
|
||||||
|
fun whitePointD65(): DoubleArray {
|
||||||
|
return WHITE_POINT_D65
|
||||||
|
}
|
||||||
|
|
||||||
|
fun labF(t: Double): Double {
|
||||||
|
val e = 216.0 / 24389.0
|
||||||
|
val kappa = 24389.0 / 27.0
|
||||||
|
return if (t > e) {
|
||||||
|
t.pow(1.0 / 3.0)
|
||||||
|
} else {
|
||||||
|
(kappa * t + 16) / 116
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun labInvf(ft: Double): Double {
|
||||||
|
val e = 216.0 / 24389.0
|
||||||
|
val kappa = 24389.0 / 27.0
|
||||||
|
val ft3 = ft * ft * ft
|
||||||
|
return if (ft3 > e) {
|
||||||
|
ft3
|
||||||
|
} else {
|
||||||
|
(116 * ft - 16) / kappa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This file is automatically generated. Do not modify it.
|
|
||||||
|
|
||||||
package utils;
|
|
||||||
|
|
||||||
/** Utility methods for mathematical operations. */
|
|
||||||
public class MathUtils {
|
|
||||||
private MathUtils() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The signum function.
|
|
||||||
*
|
|
||||||
* @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
|
|
||||||
*/
|
|
||||||
public static int signum(double num) {
|
|
||||||
if (num < 0) {
|
|
||||||
return -1;
|
|
||||||
} else if (num == 0) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The linear interpolation function.
|
|
||||||
*
|
|
||||||
* @return start if amount = 0 and stop if amount = 1
|
|
||||||
*/
|
|
||||||
public static double lerp(double start, double stop, double amount) {
|
|
||||||
return (1.0 - amount) * start + amount * stop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clamps an integer between two integers.
|
|
||||||
*
|
|
||||||
* @return input when min <= input <= max, and either min or max otherwise.
|
|
||||||
*/
|
|
||||||
public static int clampInt(int min, int max, int input) {
|
|
||||||
if (input < min) {
|
|
||||||
return min;
|
|
||||||
} else if (input > max) {
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clamps an integer between two floating-point numbers.
|
|
||||||
*
|
|
||||||
* @return input when min <= input <= max, and either min or max otherwise.
|
|
||||||
*/
|
|
||||||
public static double clampDouble(double min, double max, double input) {
|
|
||||||
if (input < min) {
|
|
||||||
return min;
|
|
||||||
} else if (input > max) {
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes a degree measure as an integer.
|
|
||||||
*
|
|
||||||
* @return a degree measure between 0 (inclusive) and 360 (exclusive).
|
|
||||||
*/
|
|
||||||
public static int sanitizeDegreesInt(int degrees) {
|
|
||||||
degrees = degrees % 360;
|
|
||||||
if (degrees < 0) {
|
|
||||||
degrees = degrees + 360;
|
|
||||||
}
|
|
||||||
return degrees;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes a degree measure as a floating-point number.
|
|
||||||
*
|
|
||||||
* @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
|
|
||||||
*/
|
|
||||||
public static double sanitizeDegreesDouble(double degrees) {
|
|
||||||
degrees = degrees % 360.0;
|
|
||||||
if (degrees < 0) {
|
|
||||||
degrees = degrees + 360.0;
|
|
||||||
}
|
|
||||||
return degrees;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign of direction change needed to travel from one angle to another.
|
|
||||||
*
|
|
||||||
* <p>For angles that are 180 degrees apart from each other, both directions have the same travel
|
|
||||||
* distance, so either direction is shortest. The value 1.0 is returned in this case.
|
|
||||||
*
|
|
||||||
* @param from The angle travel starts from, in degrees.
|
|
||||||
* @param to The angle travel ends at, in degrees.
|
|
||||||
* @return -1 if decreasing from leads to the shortest travel distance, 1 if increasing from leads
|
|
||||||
* to the shortest travel distance.
|
|
||||||
*/
|
|
||||||
public static double rotationDirection(double from, double to) {
|
|
||||||
double increasingDifference = sanitizeDegreesDouble(to - from);
|
|
||||||
return increasingDifference <= 180.0 ? 1.0 : -1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Distance of two points on a circle, represented using degrees. */
|
|
||||||
public static double differenceDegrees(double a, double b) {
|
|
||||||
return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Multiplies a 1x3 row vector with a 3x3 matrix. */
|
|
||||||
public static double[] matrixMultiply(double[] row, double[][] matrix) {
|
|
||||||
double a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
|
|
||||||
double b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
|
|
||||||
double c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
|
|
||||||
return new double[] {a, b, c};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
// This file is automatically generated. Do not modify it.
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/** Utility methods for mathematical operations. */
|
||||||
|
object MathUtils {
|
||||||
|
/**
|
||||||
|
* The linear interpolation function.
|
||||||
|
*
|
||||||
|
* @return start if amount = 0 and stop if amount = 1
|
||||||
|
*/
|
||||||
|
fun lerp(start: Double, stop: Double, amount: Double): Double {
|
||||||
|
return (1.0 - amount) * start + amount * stop
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a degree measure as an integer.
|
||||||
|
*
|
||||||
|
* @return a degree measure between 0 (inclusive) and 360 (exclusive).
|
||||||
|
*/
|
||||||
|
fun sanitizeDegreesInt(degrees: Int): Int {
|
||||||
|
var degrees = degrees % 360
|
||||||
|
if (degrees < 0) {
|
||||||
|
degrees += 360
|
||||||
|
}
|
||||||
|
return degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a degree measure as a floating-point number.
|
||||||
|
*
|
||||||
|
* @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
|
||||||
|
*/
|
||||||
|
fun sanitizeDegreesDouble(degrees: Double): Double {
|
||||||
|
var degrees = degrees % 360.0
|
||||||
|
if (degrees < 0) {
|
||||||
|
degrees += 360.0
|
||||||
|
}
|
||||||
|
return degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign of direction change needed to travel from one angle to another.
|
||||||
|
*
|
||||||
|
* For angles that are 180 degrees apart from each other, both directions have the same travel
|
||||||
|
* distance, so either direction is shortest. The value 1.0 is returned in this case.
|
||||||
|
*
|
||||||
|
* @param from The angle travel starts from, in degrees.
|
||||||
|
* @param to The angle travel ends at, in degrees.
|
||||||
|
* @return -1 if decreasing from leads to the shortest travel distance, 1 if increasing from leads
|
||||||
|
* to the shortest travel distance.
|
||||||
|
*/
|
||||||
|
fun rotationDirection(from: Double, to: Double): Double {
|
||||||
|
val increasingDifference = sanitizeDegreesDouble(to - from)
|
||||||
|
return if (increasingDifference <= 180.0) 1.0 else -1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Distance of two points on a circle, represented using degrees. */
|
||||||
|
fun differenceDegrees(a: Double, b: Double): Double {
|
||||||
|
return 180.0 - abs(abs(a - b) - 180.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Multiplies a 1x3 row vector with a 3x3 matrix. */
|
||||||
|
fun matrixMultiply(row: DoubleArray, matrix: Array<DoubleArray>): DoubleArray {
|
||||||
|
val a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2]
|
||||||
|
val b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2]
|
||||||
|
val c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2]
|
||||||
|
return doubleArrayOf(a, b, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package utils
|
||||||
|
|
||||||
|
/** Utility methods for string representations of colors. */
|
||||||
|
internal object StringUtils {
|
||||||
|
/**
|
||||||
|
* Hex string representing color, ex. #ff0000 for red.
|
||||||
|
*
|
||||||
|
* @param argb ARGB representation of a color.
|
||||||
|
*/
|
||||||
|
fun hexFromArgb(argb: Int): String {
|
||||||
|
val red = ColorUtils.redFromArgb(argb)
|
||||||
|
val blue = ColorUtils.blueFromArgb(argb)
|
||||||
|
val green = ColorUtils.greenFromArgb(argb)
|
||||||
|
return String.format("#%02x%02x%02x", red, green, blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user