Update default colors
This commit is contained in:
@@ -29,7 +29,7 @@ fun GuardedPreference(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||
) {
|
||||
if (locked) {
|
||||
Banner(
|
||||
|
||||
@@ -23,7 +23,7 @@ fun <T> ListPreference(
|
||||
items: List<ListPreferenceItem<T>>,
|
||||
value: T,
|
||||
summary: String? = items.firstOrNull { value == it.value }?.label,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||
onValueChanged: (T) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
itemLabel: @Composable RowScope.(item: ListPreferenceItem<T>) -> Unit = {
|
||||
|
||||
@@ -29,7 +29,7 @@ fun Preference(
|
||||
onClick: () -> Unit = {},
|
||||
controls: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -92,7 +92,7 @@ fun Preference(
|
||||
onClick: () -> Unit = {},
|
||||
controls: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||
) {
|
||||
Preference(
|
||||
title = {
|
||||
@@ -121,7 +121,7 @@ fun Preference(
|
||||
onClick: () -> Unit = {},
|
||||
controls: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||
) {
|
||||
Preference(
|
||||
title,
|
||||
|
||||
@@ -28,13 +28,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -157,8 +153,8 @@ fun PreferenceScreen(
|
||||
content = content,
|
||||
verticalArrangement = verticalArrangement,
|
||||
contentPadding = PaddingValues(
|
||||
top = it.calculateTopPadding(),
|
||||
bottom = it.calculateBottomPadding() + 4.dp,
|
||||
top = it.calculateTopPadding() + 12.dp,
|
||||
bottom = it.calculateBottomPadding() + 12.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ fun PreferenceWithSwitch(
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.extraSmall)
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
|
||||
@@ -38,7 +38,7 @@ fun SliderPreference(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.extraSmall)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall)
|
||||
.padding(
|
||||
start = if (icon != null || iconPadding) 8.dp else 16.dp,
|
||||
end = 16.dp,
|
||||
|
||||
@@ -21,7 +21,7 @@ fun SwitchPreference(
|
||||
value: Boolean,
|
||||
onValueChanged: (Boolean) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceBright,
|
||||
) {
|
||||
Preference(
|
||||
title = title,
|
||||
|
||||
@@ -24,7 +24,7 @@ fun Color.Companion.hct(hue: Float, chroma: Float, tone: Float): Color {
|
||||
fun Color.atTone(tone: Int): Color {
|
||||
return Color(
|
||||
Hct.fromInt(this.toArgb()).apply {
|
||||
this.tone = tone.toDouble()
|
||||
this.setTone(tone.toDouble())
|
||||
}.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ fun ColorSchemePreferenceCategory(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
MaterialTheme.shapes.extraSmall
|
||||
)
|
||||
.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.lightColorSchemeOf
|
||||
import de.mm20.launcher2.ui.theme.colorscheme.systemCorePalette
|
||||
import hct.Hct
|
||||
import kotlinx.serialization.Serializable
|
||||
import palettes.CorePalette
|
||||
import scheme.SchemeExpressive
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Serializable
|
||||
data class ColorSchemeSettingsRoute(
|
||||
@Serializable(with = UUIDSerializer::class) val id: UUID
|
||||
): NavKey
|
||||
) : NavKey
|
||||
|
||||
@Composable
|
||||
fun ColorSchemeSettingsScreen(themeId: UUID) {
|
||||
@@ -91,7 +91,7 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
||||
val previewColorScheme =
|
||||
theme?.let { if (previewDarkTheme) darkColorSchemeOf(it) else lightColorSchemeOf(it) }
|
||||
|
||||
val systemPalette = systemCorePalette()
|
||||
val systemPalette = systemCorePalette(dark)
|
||||
|
||||
val mergedCorePalette by remember(theme?.corePalette, systemPalette) {
|
||||
derivedStateOf {
|
||||
@@ -175,7 +175,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
|
||||
defaultValue = systemPalette.secondary,
|
||||
autoGenerate = {
|
||||
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,
|
||||
autoGenerate = {
|
||||
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,
|
||||
autoGenerate = {
|
||||
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,
|
||||
autoGenerate = {
|
||||
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,
|
||||
autoGenerate = {
|
||||
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(),
|
||||
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(
|
||||
onClick = { },
|
||||
onClick = { },
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
|
||||
@@ -35,13 +35,13 @@ fun ColorSwatch(
|
||||
else 40f
|
||||
}
|
||||
it.apply {
|
||||
this.tone = tone.toDouble()
|
||||
this.setTone(tone.toDouble())
|
||||
}.toInt()
|
||||
})
|
||||
val borderColor = Color(Hct.fromInt(color.toArgb()).let {
|
||||
val tone = if (darkTheme) 30f else 80f
|
||||
it.apply {
|
||||
this.tone = tone.toDouble()
|
||||
this.setTone(tone.toDouble())
|
||||
}.toInt()
|
||||
})
|
||||
Box(
|
||||
|
||||
@@ -58,7 +58,7 @@ fun CorePaletteColorPreference(
|
||||
.clickable(
|
||||
onClick = { showDialog = true },
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
@@ -81,7 +81,7 @@ fun ThemeColorPreference(
|
||||
.clickable(
|
||||
onClick = { showDialog = true },
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
@@ -57,7 +57,7 @@ fun CrashReportScreen(fileName: String) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||
.horizontalScroll(
|
||||
rememberScrollState()
|
||||
),
|
||||
@@ -80,7 +80,7 @@ fun CrashReportScreen(fileName: String) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.medium)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text(text = "Device Information", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
@@ -154,7 +154,7 @@ fun IconsSettingsScreen() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
MaterialTheme.shapes.extraSmall
|
||||
)
|
||||
.padding(vertical = 24.dp, horizontal = 8.dp)
|
||||
|
||||
@@ -414,7 +414,7 @@ fun ShapePreference(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||
.clickable(
|
||||
onClick = { showDialog = true },
|
||||
)
|
||||
@@ -733,7 +733,7 @@ private fun ShapePreview(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
@@ -232,7 +232,7 @@ private fun TransparencyPreference(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.background(MaterialTheme.colorScheme.surfaceBright)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
@@ -777,7 +777,7 @@ private fun FontPreference(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.background(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
MaterialTheme.colorScheme.surfaceBright,
|
||||
MaterialTheme.shapes.extraSmall
|
||||
)
|
||||
) {
|
||||
@@ -1309,7 +1309,7 @@ private fun TypographyPreview(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
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.merge
|
||||
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
||||
import dynamiccolor.ColorSpec
|
||||
import hct.Hct
|
||||
import org.koin.compose.koinInject
|
||||
import scheme.SchemeContent
|
||||
import scheme.SchemeExpressive
|
||||
import scheme.SchemeNeutral
|
||||
import scheme.SchemeTonalSpot
|
||||
|
||||
@Composable
|
||||
fun lightColorSchemeOf(colors: ThemeColors): ColorScheme {
|
||||
return colorSchemeOf(colors.lightColorScheme.merge(DefaultLightColorScheme), colors.corePalette)
|
||||
return colorSchemeOf(false, colors.lightColorScheme.merge(DefaultLightColorScheme), colors.corePalette)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun darkColorSchemeOf(colors: ThemeColors): ColorScheme {
|
||||
return colorSchemeOf(colors.darkColorScheme.merge(DefaultDarkColorScheme), colors.corePalette)
|
||||
return colorSchemeOf(true, colors.darkColorScheme.merge(DefaultDarkColorScheme), colors.corePalette)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette): ColorScheme {
|
||||
val defaultPalette = systemCorePalette()
|
||||
fun colorSchemeOf(dark: Boolean, colorScheme: FullColorScheme, corePalette: PartialCorePalette): ColorScheme {
|
||||
val defaultPalette = systemCorePalette(dark)
|
||||
return remember(colorScheme, corePalette, defaultPalette) {
|
||||
val mergedCorePalette = corePalette.merge(defaultPalette)
|
||||
ColorScheme(
|
||||
@@ -79,7 +85,7 @@ fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun systemCorePalette(): CorePalette<Int> {
|
||||
fun systemCorePalette(dark: Boolean): CorePalette<Int> {
|
||||
val uiSettings: UiSettings = koinInject()
|
||||
val compatModeColors by remember {
|
||||
uiSettings.compatModeColors
|
||||
@@ -98,14 +104,20 @@ fun systemCorePalette(): CorePalette<Int> {
|
||||
}
|
||||
val wallpaperColors = LocalWallpaperColors.current
|
||||
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(
|
||||
primary = corePalette.a1.tone(40),
|
||||
secondary = corePalette.a2.tone(40),
|
||||
tertiary = corePalette.a3.tone(40),
|
||||
neutral = corePalette.n1.tone(40),
|
||||
neutralVariant = corePalette.n2.tone(40),
|
||||
error = corePalette.error.tone(40),
|
||||
primary = corePalette.primaryPaletteKeyColor,
|
||||
secondary = corePalette.secondaryPaletteKeyColor,
|
||||
tertiary = corePalette.tertiaryPaletteKeyColor,
|
||||
neutral = corePalette.neutralPaletteKeyColor,
|
||||
neutralVariant = corePalette.neutralVariantPaletteKeyColor,
|
||||
error = corePalette.errorPalette.keyColor.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ val DefaultDarkColorScheme = ColorScheme<Color>(
|
||||
onErrorContainer = ColorRef(CorePaletteColor.Error, 90),
|
||||
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
|
||||
surface = ColorRef(CorePaletteColor.Neutral, 6),
|
||||
surfaceBright = ColorRef(CorePaletteColor.Neutral, 24),
|
||||
surfaceBright = ColorRef(CorePaletteColor.Neutral, 18),
|
||||
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
|
||||
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
|
||||
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 12),
|
||||
@@ -136,7 +136,7 @@ val HighContrastDarkColorScheme = ColorScheme<Color>(
|
||||
onErrorContainer = ColorRef(CorePaletteColor.Error, 0),
|
||||
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
|
||||
surface = ColorRef(CorePaletteColor.Neutral, 6),
|
||||
surfaceBright = ColorRef(CorePaletteColor.Neutral, 24),
|
||||
surfaceBright = ColorRef(CorePaletteColor.Neutral, 18),
|
||||
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
|
||||
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
|
||||
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