Add colors switch feature

This commit is contained in:
Ash 2022-04-13 11:42:55 +08:00
parent f903238a59
commit 00231970cd
52 changed files with 2629 additions and 93 deletions

View File

@ -68,9 +68,9 @@ class App : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
CrashHandler(this)
dataStoreInit()
applicationScope.launch(dispatcherDefault) {
accountInit()
dataStoreInit()
workerInit()
}
}

View File

@ -1,6 +1,7 @@
package me.ash.reader
import android.content.Context
import android.util.Log
import android.widget.Toast
import java.lang.Thread.UncaughtExceptionHandler
import kotlin.system.exitProcess
@ -13,6 +14,7 @@ class CrashHandler(private val context: Context) : UncaughtExceptionHandler {
override fun uncaughtException(p0: Thread, p1: Throwable) {
Toast.makeText(context, p1.message, Toast.LENGTH_LONG).show()
p1.printStackTrace()
Log.e("RLog", "uncaughtException: ${p1.message}" )
android.os.Process.killProcess(android.os.Process.myPid());
exitProcess(1)
}

View File

@ -19,10 +19,12 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.ui.theme.palette.onDark
@Composable
fun Banner(
@ -33,20 +35,18 @@ fun Banner(
action: (@Composable () -> Unit)? = null,
onClick: () -> Unit = {},
) {
val lightThemeColors = MaterialTheme.colorScheme
val lightPrimaryContainer = lightThemeColors.primaryContainer
val lightOnSurface = lightThemeColors.onSurface
Surface(
modifier = modifier.fillMaxWidth().height(88.dp),
color = MaterialTheme.colorScheme.surface,
modifier = modifier
.fillMaxWidth()
.height(88.dp),
color = Color.Unspecified,
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp))
.background(lightPrimaryContainer)
.background(MaterialTheme.colorScheme.primaryContainer onDark MaterialTheme.colorScheme.onPrimaryContainer)
.clickable { onClick() }
.padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically
@ -57,26 +57,29 @@ fun Banner(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(end = 16.dp),
tint = lightOnSurface,
tint = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
)
}
}
Column(
modifier = Modifier.weight(1f).fillMaxHeight(),
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = title,
maxLines = if (desc == null) 2 else 1,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
color = lightOnSurface,
color = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
overflow = TextOverflow.Ellipsis,
)
desc?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = lightOnSurface.copy(alpha = 0.7f),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
onDark MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -84,9 +87,12 @@ fun Banner(
}
action?.let {
Box(Modifier.padding(start = 16.dp)) {
CompositionLocalProvider(LocalContentColor provides lightOnSurface) {
it()
}
CompositionLocalProvider(
LocalContentColor provides (
MaterialTheme.colorScheme.onSurface
onDark MaterialTheme.colorScheme.surface
)
) { it() }
}
}
}

View File

@ -0,0 +1,45 @@
package me.ash.reader.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import me.ash.reader.ui.theme.palette.onDark
@Composable
fun BlockButton(
modifier: Modifier = Modifier,
text: String = "",
selected: Boolean = false,
containerColor: Color = MaterialTheme.colorScheme.surface.copy(0.7f) onDark MaterialTheme.colorScheme.inverseOnSurface,
selectedContainerColor: Color = MaterialTheme.colorScheme.primaryContainer onDark MaterialTheme.colorScheme.onPrimaryContainer,
contentColor: Color = MaterialTheme.colorScheme.inverseSurface,
selectedContentColor: Color = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
onClick: () -> Unit = {},
) {
Column(
modifier = modifier
.height(56.dp)
.clip(RoundedCornerShape(12.dp))
.background(if (selected) selectedContainerColor else containerColor)
.clickable(onClick = onClick),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
color = if (selected) selectedContentColor else contentColor,
)
}
}

View File

@ -0,0 +1,45 @@
package me.ash.reader.ui.component
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun BlockButtonRadios(
modifier: Modifier = Modifier,
selected: Int = 0,
onSelected: (Int) -> Unit,
items: List<BlockButtonRadiosItem> = listOf(),
) {
Column {
Row(
modifier = modifier.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
items.forEachIndexed { index, item ->
BlockButton(
modifier = Modifier
.weight(1f)
.padding(end = if (item == items.last()) 0.dp else 8.dp),
text = item.text,
selected = selected == index,
) {
onSelected(index)
item.onClick()
}
}
}
Spacer(modifier = Modifier.height(24.dp))
items[selected].content()
}
}
data class BlockButtonRadiosItem(
val text: String,
val onClick: () -> Unit = {},
val content: @Composable () -> Unit,
)

View File

@ -0,0 +1,106 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
* @modifier Ashinch
*/
package me.ash.reader.ui.component
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.ui.theme.palette.onDark
import me.ash.reader.ui.theme.palette.tonalPalettes
// TODO: ripple & swipe
@Composable
fun Switch(
modifier: Modifier = Modifier,
activated: Boolean,
enable: Boolean = true,
onClick: (() -> Unit)? = null
) {
val tonalPalettes = MaterialTheme.colorScheme.tonalPalettes()
Surface(
modifier = modifier.size(56.dp, 28.dp).alpha(if (enable) 1f else 0.5f),
shape = CircleShape,
color = animateColorAsState(
if (activated) (tonalPalettes primary 40) onDark (tonalPalettes neutralVariant 50)
else (tonalPalettes neutralVariant 50) onDark (tonalPalettes neutral 60)
).value
) {
Box(
modifier = Modifier.fillMaxSize()
then if (onClick != null) Modifier.clickable { onClick() } else Modifier
) {
Surface(
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart)
.offset(x = animateDpAsState(if (activated) 32.dp else 4.dp).value),
shape = CircleShape,
color = animateColorAsState(
if (activated) tonalPalettes primary 90
else (tonalPalettes neutralVariant 70) onDark (tonalPalettes neutral 30)
).value
) {}
}
}
}
// TODO: inactivated colors
@Composable
fun SwitchHeadline(
activated: Boolean,
onClick: () -> Unit,
title: String,
modifier: Modifier = Modifier
) {
val tonalPalettes = MaterialTheme.colorScheme.tonalPalettes()
Surface(
modifier = modifier,
color = Color.Unspecified,
contentColor = tonalPalettes neutral 10,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(24.dp))
.background(tonalPalettes primary 90)
.clickable { onClick() }
.padding(20.dp, 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
maxLines = 2,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp)
)
}
Box(Modifier.padding(start = 20.dp)) {
Switch(activated = activated)
}
}
}
}

View File

@ -18,6 +18,10 @@ val Context.currentAccountId: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
val Context.currentAccountType: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
val Context.themeIndex: Int
get() = this.dataStore.get(DataStoreKeys.ThemeIndex) ?: 0
val Context.customPrimaryColor: String
get() = this.dataStore.get(DataStoreKeys.CustomPrimaryColor) ?: ""
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
this.edit {
@ -25,6 +29,14 @@ suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, valu
}
}
fun <T> DataStore<Preferences>.putBlocking(dataStoreKeys: DataStoreKeys<T>, value: T) {
runBlocking {
this@putBlocking.edit {
it[dataStoreKeys.key] = value
}
}
}
@Suppress("UNCHECKED_CAST")
fun <T> DataStore<Preferences>.get(dataStoreKeys: DataStoreKeys<T>): T? {
return runBlocking {
@ -59,4 +71,14 @@ sealed class DataStoreKeys<T> {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("currentAccountType")
}
object ThemeIndex : DataStoreKeys<Int>() {
override val key: Preferences.Key<Int>
get() = intPreferencesKey("themeIndex")
}
object CustomPrimaryColor : DataStoreKeys<String>() {
override val key: Preferences.Key<String>
get() = stringPreferencesKey("customPrimaryColor")
}
}

View File

@ -14,13 +14,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsHeight
import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.ui.ext.animatedComposable
import me.ash.reader.ui.ext.isFirstLaunch
import me.ash.reader.ui.page.home.HomePage
import me.ash.reader.ui.page.settings.ColorAndStyle
import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.page.startup.StartupPage
import me.ash.reader.ui.theme.AppTheme
@ -38,11 +38,11 @@ fun HomeEntry() {
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
}
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
Column {
Row(
modifier = Modifier
.weight(1f)
.statusBarsPadding()
.background(MaterialTheme.colorScheme.surface),
) {
AnimatedNavHost(
navController = navController,
@ -57,12 +57,16 @@ fun HomeEntry() {
animatedComposable(route = RouteName.SETTINGS) {
SettingsPage(navController)
}
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
ColorAndStyle(navController)
}
}
}
Spacer(
modifier = Modifier
.navigationBarsHeight()
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
)
}
}

View File

@ -7,4 +7,5 @@ object RouteName {
const val ARTICLE = "article"
const val READ = "read"
const val SETTINGS = "settings"
const val COLOR_AND_STYLE = "color_and_style"
}

View File

@ -54,13 +54,13 @@ fun FilterBar(
view.playSoundEffect(SoundEffectConstants.CLICK)
filterOnClick(item)
},
// colors = NavigationBarItemDefaults.colors(
colors = NavigationBarItemDefaults.colors(
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
// unselectedIconColor = MaterialTheme.colorScheme.outline,
// selectedTextColor = MaterialTheme.colorScheme.onSurface,
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
// indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
// )
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
)
)
}
Spacer(modifier = Modifier.width(60.dp))

View File

@ -1,13 +1,16 @@
package me.ash.reader.ui.page.home
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.launch
import me.ash.reader.ui.component.ViewPager
@ -91,7 +94,11 @@ fun HomePage(
)
}
Column {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.statusBarsPadding(),
) {
ViewPager(
modifier = Modifier.weight(1f),
state = viewState.pagerState,

View File

@ -0,0 +1,340 @@
package me.ash.reader.ui.page.settings
import android.annotation.SuppressLint
import android.os.Build
import androidx.compose.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.ui.component.*
import me.ash.reader.ui.ext.*
import me.ash.reader.ui.theme.palette.*
import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes
import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper
@SuppressLint("FlowOperatorInvokedInComposition")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColorAndStyle(
navController: NavHostController,
) {
val context = LocalContext.current
val wallpaperTonalPalettes = extractTonalPalettesFromUserWallpaper()
var radioButtonSelected by remember { mutableStateOf(if (context.themeIndex > 4) 0 else 1) }
Scaffold(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
.statusBarsPadding(),
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
topBar = {
SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface
),
title = {},
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {
navController.popBackStack()
}
},
actions = {}
)
},
content = {
LazyColumn {
item {
DisplayText(text = stringResource(R.string.color_and_style), desc = "")
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.aspectRatio(1.38f)
.clip(RoundedCornerShape(24.dp))
.background(
MaterialTheme.colorScheme.inverseOnSurface
onLight MaterialTheme.colorScheme.surface.copy(0.7f)
)
.clickable { },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(60.dp),
painter = painterResource(id = R.drawable.palettie),
contentDescription = stringResource(R.string.welcome),
)
}
Spacer(modifier = Modifier.height(24.dp))
}
item {
BlockButtonRadios(
selected = radioButtonSelected,
onSelected = { radioButtonSelected = it },
items = listOf(
BlockButtonRadiosItem(
text = stringResource(R.string.wallpaper_colors),
onClick = {},
) {
Palettes(
palettes = wallpaperTonalPalettes.run {
if (this.size > 5) {
this.subList(5, wallpaperTonalPalettes.size)
} else {
emptyList()
}
},
themeIndexPrefix = 5,
)
},
BlockButtonRadiosItem(
text = stringResource(R.string.basic_colors),
onClick = {},
) {
Palettes(
palettes = wallpaperTonalPalettes.subList(0, 5)
)
},
),
)
Spacer(modifier = Modifier.height(24.dp))
}
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.style)
)
SettingItem(
title = stringResource(R.string.dark_theme),
desc = stringResource(R.string.use_device_theme),
enable = false,
separatedActions = true,
onClick = {},
) {
Switch(activated = isSystemInDarkTheme(), enable = false)
}
SettingItem(
title = stringResource(R.string.tonal_elevation),
enable = false,
onClick = {},
) {}
Spacer(modifier = Modifier.height(24.dp))
}
item {
Subtitle(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.fonts)
)
SettingItem(
title = stringResource(R.string.basic_fonts),
desc = "Google Sans",
enable = false,
onClick = {},
) {}
SettingItem(
title = stringResource(R.string.reading_fonts),
desc = "Google Sans",
enable = false,
onClick = {},
) {}
SettingItem(
title = stringResource(R.string.reading_fonts_size),
desc = "16sp",
enable = false,
onClick = {},
) {}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
)
}
@SuppressLint("FlowOperatorInvokedInComposition")
@Composable
fun Palettes(
modifier: Modifier = Modifier,
palettes: List<TonalPalettes>,
themeIndexPrefix: Int = 0,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val themeIndex = context.dataStore.data
.map { it[DataStoreKeys.ThemeIndex.key] ?: 0 }
.collectAsState(initial = 0).value
val customPrimaryColor = context.dataStore.data
.map { it[DataStoreKeys.CustomPrimaryColor.key] ?: "" }
.collectAsState(initial = "").value
val tonalPalettes = customPrimaryColor.safeHexToColor().toTonalPalettes()
var addDialogVisible by remember { mutableStateOf(false) }
var customColorValue by remember { mutableStateOf(context.customPrimaryColor) }
if (palettes.isEmpty()) {
Row(
modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth()
.height(80.dp)
.clip(RoundedCornerShape(16.dp))
.background(
MaterialTheme.colorScheme.inverseOnSurface
onLight MaterialTheme.colorScheme.surface.copy(0.7f),
)
.clickable {},
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1)
stringResource(R.string.no_palettes)
else stringResource(R.string.only_android_8_1_plus),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.inverseSurface,
)
}
} else {
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
palettes.forEachIndexed { index, palette ->
val isCustom = index == palettes.lastIndex && themeIndexPrefix == 0
val i = themeIndex - themeIndexPrefix
SelectableMiniPalette(
selected = if (i >= palettes.size) i == 0 else i == index,
isCustom = isCustom,
onClick = {
if (isCustom) {
addDialogVisible = true
} else {
scope.launch(Dispatchers.IO) {
context.dataStore.put(
DataStoreKeys.ThemeIndex,
themeIndexPrefix + index
)
}
}
},
palette = if (isCustom) tonalPalettes else palette
)
}
}
}
TextFieldDialog(
visible = addDialogVisible,
title = "强调色",
icon = Icons.Outlined.Palette,
value = customColorValue,
placeholder = "#123456",
onValueChange = {
customColorValue = it
},
onDismissRequest = {
addDialogVisible = false
customColorValue = context.customPrimaryColor
},
onConfirm = {
it.checkColorHex()?.let {
scope.launch(Dispatchers.IO) {
context.dataStore.put(DataStoreKeys.CustomPrimaryColor, it)
context.dataStore.put(DataStoreKeys.ThemeIndex, 4)
}
addDialogVisible = false
customColorValue = it
}
}
)
}
@Composable
fun SelectableMiniPalette(
modifier: Modifier = Modifier,
selected: Boolean,
isCustom: Boolean = false,
onClick: () -> Unit,
palette: TonalPalettes,
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
color = if (isCustom) {
MaterialTheme.colorScheme.primaryContainer
.copy(0.5f) onDark MaterialTheme.colorScheme.onPrimaryContainer.copy(0.3f)
} else {
MaterialTheme.colorScheme
.inverseOnSurface onLight MaterialTheme.colorScheme.surface.copy(0.7f)
},
) {
Surface(
modifier = Modifier
.clickable { onClick() }
.padding(16.dp)
.size(48.dp),
shape = CircleShape,
color = palette primary 90,
) {
Box {
Surface(
modifier = Modifier
.size(48.dp)
.offset((-24).dp, 24.dp),
color = palette tertiary 90,
) {}
Surface(
modifier = Modifier
.size(48.dp)
.offset(24.dp, 24.dp),
color = palette secondary 50,
) {}
AnimatedVisibility(
visible = selected,
modifier = Modifier
.align(Alignment.Center)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
enter = fadeIn() + expandIn(expandFrom = Alignment.Center),
exit = shrinkOut(shrinkTowards = Alignment.Center) + fadeOut()
) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "Checked",
modifier = Modifier
.padding(8.dp)
.size(16.dp),
tint = MaterialTheme.colorScheme.surface
)
}
}
}
}
}

View File

@ -6,7 +6,7 @@
* @modifier Ashinch
*/
package me.ash.reader.ui.component
package me.ash.reader.ui.page.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -22,6 +22,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@ -30,6 +31,7 @@ import androidx.compose.ui.unit.sp
@Composable
fun SelectableSettingGroupItem(
modifier: Modifier = Modifier,
enable: Boolean = true,
selected: Boolean = false,
title: String,
desc: String? = null,
@ -37,7 +39,7 @@ fun SelectableSettingGroupItem(
onClick: () -> Unit,
) {
Surface(
modifier = modifier.clickable { onClick() },
modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f),
color = Color.Unspecified,
) {
Row(
@ -45,7 +47,7 @@ fun SelectableSettingGroupItem(
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.surface,
color = if (selected) MaterialTheme.colorScheme.onSurface else Color.Unspecified,
shape = RoundedCornerShape(24.dp)
)
.padding(8.dp, 16.dp),

View File

@ -0,0 +1,81 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
* @modifier Ashinch
*/
package me.ash.reader.ui.page.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun SettingItem(
modifier: Modifier = Modifier,
enable: Boolean = true,
title: String,
desc: String? = null,
icon: ImageVector? = null,
separatedActions: Boolean = false,
onClick: () -> Unit,
action: (@Composable () -> Unit)? = null
) {
Surface(
modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f),
color = Color.Unspecified
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp, 16.dp, 16.dp, 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(end = 24.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
maxLines = if (desc == null) 2 else 1,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp)
)
desc?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
style = MaterialTheme.typography.bodyMedium,
)
}
}
action?.let {
if (separatedActions) {
Divider(
modifier = Modifier
.padding(start = 16.dp)
.size(1.dp, 32.dp)
)
}
Box(Modifier.padding(start = 16.dp)) {
it()
}
}
}
}
}

View File

@ -14,12 +14,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import me.ash.reader.R
import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.SelectableSettingGroupItem
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.theme.palette.onLight
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -27,9 +28,13 @@ fun SettingsPage(
navController: NavHostController,
) {
Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
modifier = Modifier
.background(MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface)
.statusBarsPadding(),
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
topBar = {
SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface),
title = {},
navigationIcon = {
FeedbackIconButton(
@ -67,6 +72,7 @@ fun SettingsPage(
title = stringResource(R.string.accounts),
desc = stringResource(R.string.accounts_desc),
icon = Icons.Outlined.AccountCircle,
enable = false,
) {}
}
item {
@ -74,13 +80,16 @@ fun SettingsPage(
title = stringResource(R.string.color_and_style),
desc = stringResource(R.string.color_and_style_desc),
icon = Icons.Outlined.Palette,
) {}
) {
navController.navigate(RouteName.COLOR_AND_STYLE)
}
}
item {
SelectableSettingGroupItem(
title = stringResource(R.string.interaction),
desc = stringResource(R.string.interaction_desc),
icon = Icons.Outlined.TouchApp,
enable = false,
) {}
}
item {
@ -88,6 +97,7 @@ fun SettingsPage(
title = stringResource(R.string.languages),
desc = stringResource(R.string.languages_desc),
icon = Icons.Outlined.Language,
enable = false,
) {}
}
item {
@ -95,6 +105,7 @@ fun SettingsPage(
title = stringResource(R.string.tips_and_support),
desc = stringResource(R.string.tips_and_support_desc),
icon = Icons.Outlined.TipsAndUpdates,
enable = false,
) {}
}
}

View File

@ -12,13 +12,13 @@ import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import com.ireward.htmlcompose.HtmlText
import kotlinx.coroutines.launch
import me.ash.reader.R
@ -37,7 +37,7 @@ fun StartupPage(
val scope = rememberCoroutineScope()
Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
modifier = Modifier.statusBarsPadding().background(MaterialTheme.colorScheme.surface),
topBar = {},
content = {
LazyColumn {
@ -87,13 +87,16 @@ fun StartupPage(
}
},
bottomBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
// Row(
// modifier = Modifier
// .fillMaxWidth()
// .padding(24.dp),
// horizontalArrangement = Arrangement.End,
// verticalAlignment = Alignment.CenterVertically,
// ) {
// }
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
navController.navigate(route = RouteName.HOME)
@ -110,7 +113,6 @@ fun StartupPage(
text = { Text(text = stringResource(R.string.agree_and_continue)) },
)
}
}
)
}

View File

@ -1,51 +1,54 @@
package me.ash.reader.ui.theme
import android.os.Build
import android.annotation.SuppressLint
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalContext
import me.ash.reader.ui.theme.color.PurpleColor
private val LightThemeColors = PurpleColor.lightColorScheme
private val DarkThemeColors = PurpleColor.darkColorScheme
val LocalLightThemeColors = staticCompositionLocalOf { LightThemeColors }
val LocalDarkThemeColors = staticCompositionLocalOf { DarkThemeColors }
import kotlinx.coroutines.flow.map
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.theme.palette.LocalTonalPalettes
import me.ash.reader.ui.theme.palette.TonalPalettes
import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
import me.ash.reader.ui.theme.palette.dynamic.extractTonalPalettesFromUserWallpaper
import me.ash.reader.ui.theme.palette.dynamicDarkColorScheme
import me.ash.reader.ui.theme.palette.dynamicLightColorScheme
@SuppressLint("FlowOperatorInvokedInComposition")
@Composable
fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
wallpaperPalettes: List<TonalPalettes> = extractTonalPalettesFromUserWallpaper(),
content: @Composable () -> Unit
) {
// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val light = when {
dynamicColor -> dynamicLightColorScheme(LocalContext.current)
else -> LightThemeColors
}
val dark = when {
dynamicColor -> dynamicDarkColorScheme(LocalContext.current)
else -> DarkThemeColors
}
val colorScheme = when {
useDarkTheme -> dark
else -> light
}
val context = LocalContext.current
val themeIndex = context.dataStore.data.map { it[DataStoreKeys.ThemeIndex.key] ?: 0 }
.collectAsState(initial = 0).value
ProvideZcamViewingConditions {
CompositionLocalProvider(
LocalLightThemeColors provides light,
LocalDarkThemeColors provides dark,
LocalTonalPalettes provides wallpaperPalettes[
if (themeIndex >= wallpaperPalettes.size) {
when {
wallpaperPalettes.size == 5 -> 0
wallpaperPalettes.size > 5 -> 5
else -> 0
}
} else {
themeIndex
}
]
) {
MaterialTheme(
colorScheme = colorScheme,
colorScheme =
if (useDarkTheme) dynamicDarkColorScheme()
else dynamicLightColorScheme(),
typography = AppTypography,
content = content
)
}
}
}

View File

@ -0,0 +1,94 @@
package me.ash.reader.ui.theme.palette
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@Composable
fun dynamicLightColorScheme(): ColorScheme {
val palettes = LocalTonalPalettes.current
return lightColorScheme(
primary = palettes primary 40,
onPrimary = palettes primary 100,
primaryContainer = palettes primary 90,
onPrimaryContainer = palettes primary 10,
inversePrimary = palettes primary 80,
secondary = palettes secondary 40,
onSecondary = palettes secondary 100,
secondaryContainer = palettes secondary 90,
onSecondaryContainer = palettes secondary 10,
tertiary = palettes tertiary 40,
onTertiary = palettes tertiary 100,
tertiaryContainer = palettes tertiary 90,
onTertiaryContainer = palettes tertiary 10,
background = palettes neutral 99,
onBackground = palettes neutral 10,
surface = palettes neutral 99,
onSurface = palettes neutral 10,
surfaceVariant = palettes neutralVariant 90,
onSurfaceVariant = palettes neutralVariant 30,
inverseSurface = palettes neutral 20,
inverseOnSurface = palettes neutral 95,
outline = palettes neutralVariant 50,
)
}
@Composable
fun dynamicDarkColorScheme(): ColorScheme {
val palettes = LocalTonalPalettes.current
return darkColorScheme(
primary = palettes primary 80,
onPrimary = palettes primary 20,
primaryContainer = palettes primary 30,
onPrimaryContainer = palettes primary 90,
inversePrimary = palettes primary 40,
secondary = palettes secondary 80,
onSecondary = palettes secondary 20,
secondaryContainer = palettes secondary 30,
onSecondaryContainer = palettes secondary 90,
tertiary = palettes tertiary 80,
onTertiary = palettes tertiary 20,
tertiaryContainer = palettes tertiary 30,
onTertiaryContainer = palettes tertiary 90,
background = palettes neutral 10,
onBackground = palettes neutral 90,
surface = palettes neutral 10,
onSurface = palettes neutral 90,
surfaceVariant = palettes neutralVariant 30,
onSurfaceVariant = palettes neutralVariant 80,
inverseSurface = palettes neutral 90,
inverseOnSurface = palettes neutral 20,
outline = palettes neutralVariant 60,
)
}
@Composable
fun ColorScheme.tonalPalettes() = LocalTonalPalettes.current
@Suppress("NOTHING_TO_INLINE")
@Composable
inline infix fun Color.onLight(lightColor: Color): Color =
if (!isSystemInDarkTheme()) lightColor else this
@Suppress("NOTHING_TO_INLINE")
@Composable
inline infix fun Color.onDark(darkColor: Color): Color =
if (isSystemInDarkTheme()) darkColor else this
fun String.checkColorHex(): String? {
var s = this.trim()
if (s.length > 6) {
s = s.substring(s.length - 6)
}
return "[0-9a-fA-F]{6}".toRegex().find(s)?.value
}
fun String.safeHexToColor(): Color =
try {
Color(java.lang.Long.parseLong(this, 16))
} catch (e: Exception) {
Color.Transparent
}

View File

@ -0,0 +1,116 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette
object MaterialYouStandard {
// TODO: support RGB color spaces
// calculated with error = 0.001, chroma = 100, hue in 0 until 360, hue step = 1
val sRGBLightnessChromaMap = mapOf(
0 to 7.62939453125E-4,
1 to 3.5961087544759116,
2 to 4.780750274658203,
3 to 5.583519405788845,
4 to 6.202197604709202,
5 to 6.709673139784071,
6 to 7.141865624321832,
7 to 7.5197558932834205,
8 to 7.855508592393663,
9 to 8.170108795166016,
10 to 8.478755950927734,
11 to 8.78151363796658,
12 to 9.078428480360243,
13 to 9.368563758002388,
14 to 9.65309566921658,
15 to 9.932153489854601,
16 to 10.205938551161024,
17 to 10.474622514512804,
18 to 10.737465752495659,
19 to 10.994769202338325,
20 to 11.247151692708334,
21 to 11.494865417480469,
22 to 11.737997266981337,
23 to 11.97671890258789,
24 to 12.211305830213758,
25 to 12.440338134765625,
26 to 12.664968702528212,
27 to 12.885534498426649,
28 to 13.102118174235025,
29 to 13.314840528700087,
30 to 13.523866865370008,
31 to 13.729370964898003,
32 to 13.93154568142361,
33 to 14.129519992404514,
34 to 14.323438008626303,
35 to 14.513916439480251,
36 to 14.701114230685764,
37 to 14.885203043619791,
38 to 15.066161685519749,
39 to 15.244210561116537,
40 to 15.419385698106554,
41 to 15.591801537407768,
42 to 15.761661529541016,
43 to 15.928798251681858,
44 to 16.091954973008896,
45 to 16.25248803032769,
46 to 16.410503387451172,
47 to 16.566007402208115,
48 to 16.71794679429796,
49 to 16.8612183464898,
50 to 16.995349460177952,
51 to 17.11943096584744,
52 to 17.233810424804688,
53 to 17.34021716647678,
54 to 17.43742412990994,
55 to 17.525511847601997,
56 to 17.586015065511067,
57 to 17.615225050184463,
58 to 17.616180843777126,
59 to 17.59176466200087,
60 to 17.543212042914497,
61 to 17.472801208496094,
62 to 17.381820678710938,
63 to 17.271283467610676,
64 to 17.14224073621962,
65 to 16.99536853366428,
66 to 16.831576029459637,
67 to 16.651276482476128,
68 to 16.45496368408203,
69 to 16.24362309773763,
70 to 16.024080912272137,
71 to 15.799829694959852,
72 to 15.570498572455513,
73 to 15.336426628960503,
74 to 15.097156100802952,
75 to 14.85240724351671,
76 to 14.601781633165148,
77 to 14.345453050401476,
78 to 14.08291286892361,
79 to 13.814093271891275,
80 to 13.538227081298828,
81 to 13.255418141682943,
82 to 12.964892917209202,
83 to 12.657786475287544,
84 to 12.30661392211914,
85 to 11.908126407199436,
86 to 11.460111406114367,
87 to 10.960358513726128,
88 to 10.406063927544487,
89 to 9.794313642713758,
90 to 9.12274890475803,
91 to 8.394618564181858,
92 to 7.635277642144097,
93 to 6.844251420762804,
94 to 6.025793287489149,
95 to 5.196952819824219,
96 to 4.350443945990668,
97 to 3.472722371419271,
98 to 2.5394566853841147,
99 to 1.4974000718858507,
100 to 7.62939453125E-4,
)
}

View File

@ -0,0 +1,105 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz.Companion.toIzazbz
import me.ash.reader.ui.theme.palette.core.*
/**
* The L from CIELab
*/
typealias TonalValue = Int
typealias TonalPalette = MutableMap<TonalValue, Color>
val LocalTonalPalettes = compositionLocalOf {
TonalPalettes(238.36, 15.0)
}
@Composable
private fun TonalValue.toZcamLightness(): Double =
CieLab(L = if (this != 50) toDouble() else 49.6, a = 0.0, b = 0.0).toXyz().toIzazbz()
.toZcam().Jz
data class TonalPalettes(
val hue: Double,
val primaryChroma: Double,
val primary: TonalPalette = mutableMapOf(),
val secondary: TonalPalette = mutableMapOf(),
val tertiary: TonalPalette = mutableMapOf(),
val neutral: TonalPalette = mutableMapOf(),
val neutralVariant: TonalPalette = mutableMapOf(),
val error: TonalPalette = mutableMapOf(),
) {
@Composable
infix fun primary(tone: TonalValue): Color = primary.getOrPut(tone) {
zcamLch(
L = tone.toZcamLightness(),
C = (1.2 * primaryChroma / MaterialYouStandard.sRGBLightnessChromaMap.maxOf { it.value })
.coerceAtLeast(1.0) * MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone),
h = hue,
).clampToRgb().toColor()
}
@Composable
infix fun secondary(tone: TonalValue): Color = secondary.getOrPut(tone) {
zcamLch(
L = tone.toZcamLightness(),
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 3.0,
h = hue,
).clampToRgb().toColor()
}
@Composable
infix fun tertiary(tone: TonalValue): Color = tertiary.getOrPut(tone) {
zcamLch(
L = tone.toZcamLightness(),
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) * 2.0 / 3.0,
h = hue + 60.0,
).clampToRgb().toColor()
}
@Composable
infix fun neutral(tone: TonalValue): Color = neutral.getOrPut(tone) {
zcamLch(
L = tone.toZcamLightness(),
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 12.0,
h = hue,
).clampToRgb().toColor()
}
@Composable
infix fun neutralVariant(tone: TonalValue): Color = neutralVariant.getOrPut(tone) {
zcamLch(
L = tone.toZcamLightness(),
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone) / 6.0,
h = hue,
).clampToRgb().toColor()
}
@Composable
infix fun error(tone: TonalValue): Color = error.getOrPut(tone) {
zcamLch(
L = tone.toZcamLightness(),
C = MaterialYouStandard.sRGBLightnessChromaMap.getValue(tone),
h = 33.44,
).clampToRgb().toColor()
}
companion object {
@Composable
fun Color.toTonalPalettes(): TonalPalettes {
val zcam = toRgb().toZcam()
return TonalPalettes(hue = zcam.hz, primaryChroma = zcam.Cz)
}
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.cielab
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import kotlin.math.pow
// TODO: test
data class CieLab(
val L: Double,
val a: Double,
val b: Double,
) {
fun toXyz(
whitePoint: CieXyz,
luminance: Double
): CieXyz {
val lp = (L + 16.0) / 116.0
val absoluteWhitePoint = whitePoint * luminance
return CieXyz(
x = absoluteWhitePoint.x * fInv(lp + (a / 500.0)),
y = absoluteWhitePoint.y * fInv(lp),
z = absoluteWhitePoint.z * fInv(lp - (b / 200.0)),
)
}
companion object {
private fun f(x: Double) = when {
x > 216.0 / 24389.0 -> x.pow(1.0 / 3.0)
else -> x / (108.0 / 841.0) + 4.0 / 29.0
}
private fun fInv(x: Double): Double = when {
x > 6.0 / 29.0 -> x.pow(3.0)
else -> 108.0 / 841.0 * (x - 4.0 / 29.0)
}
fun CieXyz.toCieLab(
whitePoint: CieXyz,
luminance: Double
): CieLab {
val relativeWhitePoint = whitePoint / luminance
return CieLab(
L = 116.0 * f(y / relativeWhitePoint.y) - 16.0,
a = 500.0 * (f(x / relativeWhitePoint.x) - f(y / relativeWhitePoint.y)),
b = 200.0 * (f(y / relativeWhitePoint.y) - f(z / relativeWhitePoint.z)),
)
}
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.cielab
import me.ash.reader.ui.theme.palette.util.square
import me.ash.reader.ui.theme.palette.util.toDegrees
import me.ash.reader.ui.theme.palette.util.toRadians
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
data class CieLch(
val L: Double,
val C: Double,
val h: Double,
) {
fun toCieLab(): CieLab {
val hRad = h.toRadians()
return CieLab(
L = L,
a = C * cos(hRad),
b = C * sin(hRad),
)
}
companion object {
fun CieLab.toCieLch(): CieLch = CieLch(
L = L,
C = sqrt(square(a) + square(b)),
h = atan2(b, a).toDegrees().mod(360.0),
)
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.ciexyz
import me.ash.reader.ui.theme.palette.util.div
import me.ash.reader.ui.theme.palette.util.times
data class CieXyz(
val x: Double,
val y: Double,
val z: Double,
) {
inline val xyz: DoubleArray
get() = doubleArrayOf(x, y, z)
inline val luminance: Double
get() = y
operator fun times(luminance: Double): CieXyz = (xyz * luminance).asXyz()
operator fun div(luminance: Double): CieXyz = (xyz / luminance).asXyz()
companion object {
internal fun DoubleArray.asXyz(): CieXyz = CieXyz(this[0], this[1], this[2])
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.jzazbz
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.util.Matrix3
import kotlin.math.pow
data class Jzazbz(
val Jz: Double,
val az: Double,
val bz: Double,
) {
fun toXyz(): CieXyz {
val (x_, y_, z) = lmsToXyz * (
IzazbzToLms * doubleArrayOf(
(Jz + d_0) / (1.0 + d - d * (Jz + d_0)),
az,
bz,
)
).map {
10000.0 * ((c_1 - it.pow(1.0 / p)) / (c_3 * it.pow(1.0 / p) - c_2)).pow(1.0 / n)
}.toDoubleArray()
val x = (x_ + (b - 1.0) * z) / b
val y = (y_ + (g - 1.0) * x) / g
return CieXyz(
x = x,
y = y,
z = z,
)
}
companion object {
private const val b = 1.15
private const val g = 0.66
private const val c_1 = 3424.0 / 4096.0
private const val c_2 = 2413.0 / 128.0
private const val c_3 = 2392.0 / 128.0
private const val n = 2610.0 / 16384.0
private const val p = 1.7 * 2523.0 / 32.0
private const val d = -0.56
private const val d_0 = 1.6295499532821566E-11
private val xyzToLms: Matrix3 = Matrix3(
doubleArrayOf(0.41478972, 0.579999, 0.01464800),
doubleArrayOf(-0.2015100, 1.120649, 0.05310080),
doubleArrayOf(-0.0166008, 0.264800, 0.66847990),
)
private val lmsToXyz: Matrix3 = xyzToLms.inverse()
private val lmsToIzazbz: Matrix3 = Matrix3(
doubleArrayOf(0.5, 0.5, 0.0),
doubleArrayOf(3.524000, -4.066708, 0.542708),
doubleArrayOf(0.199076, 1.096799, -1.295875),
)
private val IzazbzToLms: Matrix3 = lmsToIzazbz.inverse()
fun CieXyz.toJzazbz(): Jzazbz {
val (Iz, az, bz) = lmsToIzazbz * (
xyzToLms * doubleArrayOf(
b * x - (b - 1.0) * z,
g * y - (g - 1.0) * x,
z,
)
).map {
((c_1 + c_2 * (it / 10000.0).pow(n)) / (1.0 + c_3 * (it / 10000.0).pow(n))).pow(p)
}.toDoubleArray()
return Jzazbz(
Jz = (1.0 + d) * Iz / (1.0 + d * Iz) - d_0,
az = az,
bz = bz,
)
}
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.jzazbz
import me.ash.reader.ui.theme.palette.util.square
import me.ash.reader.ui.theme.palette.util.toDegrees
import me.ash.reader.ui.theme.palette.util.toRadians
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
data class Jzczhz(
val Jz: Double,
val Cz: Double,
val hz: Double,
) {
fun toJzazbz(): Jzazbz {
val hRad = hz.toRadians()
return Jzazbz(
Jz = Jz,
az = Cz * cos(hRad),
bz = Cz * sin(hRad),
)
}
fun dE(other: Jzczhz): Double =
sqrt(square(Jz - other.Jz) + square(Cz - other.Cz) + 4.0 * Cz * other.Cz * square(sin((hz - other.hz) / 2.0)))
companion object {
fun Jzazbz.toJzczhz(): Jzczhz = Jzczhz(
Jz = Jz,
Cz = sqrt(square(az) + square(bz)),
hz = atan2(bz, az).toDegrees().mod(360.0),
)
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.oklab
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz.Companion.asXyz
import me.ash.reader.ui.theme.palette.util.Matrix3
import kotlin.math.pow
data class Oklab(
val L: Double,
val a: Double,
val b: Double,
) {
fun toXyz(): CieXyz = (lmsToXyz * (oklabToLms * doubleArrayOf(L, a, b)).map { it.pow(3.0) }
.toDoubleArray()).asXyz()
companion object {
private val xyzToLms: Matrix3 = Matrix3(
doubleArrayOf(0.8189330101, 0.3618667424, -0.1288597137),
doubleArrayOf(0.0329845436, 0.9293118715, 0.0361456387),
doubleArrayOf(0.0482003018, 0.2643662691, 0.6338517070),
)
private val lmsToXyz: Matrix3 = xyzToLms.inverse()
private val lmsToOklab: Matrix3 = Matrix3(
doubleArrayOf(0.2104542553, 0.7936177850, -0.0040720468),
doubleArrayOf(1.9779984951, -2.4285922050, 0.4505937099),
doubleArrayOf(0.0259040371, 0.7827717662, -0.8086757660),
)
private val oklabToLms: Matrix3 = lmsToOklab.inverse()
fun CieXyz.toOklab(): Oklab =
(lmsToOklab * (xyzToLms * xyz).map { it.pow(1.0 / 3.0) }.toDoubleArray()).asOklab()
internal fun DoubleArray.asOklab(): Oklab = Oklab(this[0], this[1], this[2])
}
}

View File

@ -0,0 +1,76 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.oklab
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb.Companion.toRgb
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
import me.ash.reader.ui.theme.palette.util.square
import me.ash.reader.ui.theme.palette.util.toDegrees
import me.ash.reader.ui.theme.palette.util.toRadians
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
data class Oklch(
val L: Double,
val C: Double,
val h: Double,
) {
fun toOklab(): Oklab {
val hRad = h.toRadians()
return Oklab(
L = L,
a = C * cos(hRad),
b = C * sin(hRad),
)
}
fun clampToRgb(colorSpace: RgbColorSpace): Rgb =
toOklab().toXyz().toRgb(1.0, colorSpace).takeIf { it.isInGamut() } ?: copy(
C = findChromaBoundaryInRgb(
colorSpace,
0.001
)
).toOklab().toXyz().toRgb(1.0, colorSpace).clamp()
private fun findChromaBoundaryInRgb(
colorSpace: RgbColorSpace,
error: Double
): Double = chromaBoundary.getOrPut(Triple(colorSpace.hashCode(), h, L)) {
var low = 0.0
var high = C
var current = this
while (high - low >= error) {
val mid = (low + high) / 2.0
current = copy(C = mid)
if (!current.toOklab().toXyz().toRgb(1.0, colorSpace).isInGamut()) {
high = mid
} else {
val next = current.copy(C = mid + error).toOklab().toXyz().toRgb(1.0, colorSpace)
if (next.isInGamut()) {
low = mid
} else {
break
}
}
}
current.C
}
companion object {
fun Oklab.toOklch(): Oklch = Oklch(
L = L,
C = sqrt(square(a) + square(b)),
h = atan2(b, a).toDegrees().mod(360.0),
)
private val chromaBoundary: MutableMap<Triple<Int, Double, Double>, Double> = mutableMapOf()
}
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.rgb
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz.Companion.asXyz
import me.ash.reader.ui.theme.palette.util.div
data class Rgb(
val r: Double,
val g: Double,
val b: Double,
val colorSpace: RgbColorSpace,
) {
inline val rgb: DoubleArray
get() = doubleArrayOf(r, g, b)
fun isInGamut(): Boolean = rgb.map { it in colorSpace.componentRange }.all { it }
fun clamp(): Rgb = rgb.map { it.coerceIn(colorSpace.componentRange) }.toDoubleArray().asRgb(colorSpace)
fun toXyz(luminance: Double): CieXyz = (
colorSpace.rgbToXyzMatrix * rgb.map {
colorSpace.transferFunction.EOTF(it)
}.toDoubleArray()
).asXyz() * luminance
override fun toString(): String = "Rgb(r=$r, g=$g, b=$b, colorSpace=${colorSpace.name})"
companion object {
fun CieXyz.toRgb(
luminance: Double,
colorSpace: RgbColorSpace
): Rgb = (colorSpace.rgbToXyzMatrix.inverse() * (xyz / luminance))
.map { colorSpace.transferFunction.OETF(it) }
.toDoubleArray().asRgb(colorSpace)
internal fun DoubleArray.asRgb(colorSpace: RgbColorSpace): Rgb = Rgb(this[0], this[1], this[2], colorSpace)
}
}

View File

@ -0,0 +1,129 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.rgb
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.GammaTransferFunction
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.HLGTransferFunction
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.PQTransferFunction
import me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction.TransferFunction
import me.ash.reader.ui.theme.palette.data.Illuminant
import me.ash.reader.ui.theme.palette.util.Matrix3
data class RgbColorSpace(
val name: String,
val componentRange: ClosedRange<Double>,
val whitePoint: CieXyz,
internal val primaries: Matrix3,
internal val transferFunction: TransferFunction,
) {
internal val rgbToXyzMatrix: Matrix3
get() {
val M1 = Matrix3(
doubleArrayOf(
primaries[0][0] / primaries[0][1],
primaries[1][0] / primaries[1][1],
primaries[2][0] / primaries[2][1],
),
doubleArrayOf(1.0, 1.0, 1.0),
doubleArrayOf(
primaries[0][2] / primaries[0][1],
primaries[1][2] / primaries[1][1],
primaries[2][2] / primaries[2][1],
)
)
val M2 = M1.inverse() * whitePoint.xyz
return Matrix3(
doubleArrayOf(M1[0][0] * M2[0], M1[0][1] * M2[1], M1[0][2] * M2[2]),
doubleArrayOf(M1[1][0] * M2[0], M1[1][1] * M2[1], M1[1][2] * M2[2]),
doubleArrayOf(M1[2][0] * M2[0], M1[2][1] * M2[1], M1[2][2] * M2[2]),
)
}
companion object {
/**
* Standard: IEC 61966-2-1
* - [Wikipedia: sRGB](https://en.wikipedia.org/wiki/SRGB)
*/
val Srgb: RgbColorSpace = RgbColorSpace(
name = "sRGB",
componentRange = 0.0..1.0,
whitePoint = Illuminant.D65,
primaries = Matrix3(
doubleArrayOf(0.64, 0.33, 0.03),
doubleArrayOf(0.30, 0.60, 0.10),
doubleArrayOf(0.15, 0.06, 0.79),
),
transferFunction = GammaTransferFunction.sRGB,
)
/**
* Standard: SMPTE EG 432-1
* - [Wikipedia: DCI-P3](https://en.wikipedia.org/wiki/DCI-P3)
*/
val DisplayP3: RgbColorSpace = RgbColorSpace(
name = "Display P3",
componentRange = 0.0..1.0,
whitePoint = Illuminant.D65,
primaries = Matrix3(
doubleArrayOf(0.68, 0.32, 0.00),
doubleArrayOf(0.265, 0.690, 0.045),
doubleArrayOf(0.15, 0.06, 0.79),
),
transferFunction = GammaTransferFunction.sRGB,
)
/**
* Standard: ITU-R BT.2020
* - [Wikipedia: Rec. 2020](https://en.wikipedia.org/wiki/Rec._2020)
*/
val BT2020: RgbColorSpace = RgbColorSpace(
name = "BT.2020",
componentRange = 0.0..1.0,
whitePoint = Illuminant.D65,
primaries = Matrix3(
doubleArrayOf(0.708, 0.292, 0.000),
doubleArrayOf(0.170, 0.797, 0.033),
doubleArrayOf(0.131, 0.046, 0.823),
),
transferFunction = GammaTransferFunction.Rec709,
)
/**
* Standard: ITU-R BT.2100
* - [Wikipedia: Rec. 2100](https://en.wikipedia.org/wiki/Rec._2100)
*/
val BT2100PQ: RgbColorSpace = RgbColorSpace(
name = "BT.2100 (PQ)",
componentRange = 0.0..1.0,
whitePoint = Illuminant.D65,
primaries = Matrix3(
doubleArrayOf(0.708, 0.292, 0.000),
doubleArrayOf(0.170, 0.797, 0.033),
doubleArrayOf(0.131, 0.046, 0.823),
),
transferFunction = PQTransferFunction(),
)
/**
* Standard: ITU-R BT.2100
* - [Wikipedia: Rec. 2100](https://en.wikipedia.org/wiki/Rec._2100)
*/
val BT2100HLG: RgbColorSpace = RgbColorSpace(
name = "BT.2100 (HLG)",
componentRange = 0.0..1.0,
whitePoint = Illuminant.D65,
primaries = Matrix3(
doubleArrayOf(0.708, 0.292, 0.000),
doubleArrayOf(0.170, 0.797, 0.033),
doubleArrayOf(0.131, 0.046, 0.823),
),
transferFunction = HLGTransferFunction(),
)
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
import kotlin.math.pow
class GammaTransferFunction(
val gamma: Double, // decoding gamma γ
val alpha: Double, // offset a, α = a + 1
val beta: Double, // linear-domain threshold β = K_0 / φ = E_t
val delta: Double, // linear gain δ = φ
) : TransferFunction {
override fun EOTF(x: Double): Double = when {
x >= beta * delta -> ((x + alpha - 1.0) / alpha).pow(gamma) // transition point βδ = K_0
else -> x / delta
}
override fun OETF(x: Double): Double = when {
x >= beta -> alpha * (x.pow(1.0 / gamma) - 1.0) + 1.0
else -> x * delta
}
companion object {
/**
* [Wikipedia: sRGB - Computing the transfer function](https://en.wikipedia.org/wiki/SRGB#Computing_the_transfer_function)
*/
val sRGB = GammaTransferFunction(
gamma = 2.4,
alpha = 1.055,
beta = 0.055 / 1.4 / ((1.055 / 2.4).pow(2.4) * (1.4 / 0.055).pow(1.4)),
// ~0.03928571428571429 / ~12.923210180787857 = ~0.0030399346397784314 -> 0.003130804935
delta = (1.055 / 2.4).pow(2.4) * (1.4 / 0.055).pow(1.4),
// ~12.923210180787857 -> 12.920020442059
)
/**
* [Rec. 709](https://www.itu.int/rec/R-REC-BT.709)
*/
val Rec709 = GammaTransferFunction(
gamma = 2.4,
alpha = 1.0 + 5.5 * 0.018053968510807, // ~1.09929682680944
beta = 0.018053968510807,
delta = 4.5,
)
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
import me.ash.reader.ui.theme.palette.util.square
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.sqrt
/**
* [Rec. 2100](https://www.itu.int/rec/R-REC-BT.2100)
*/
class HLGTransferFunction : TransferFunction {
companion object {
private val a = 0.17883277
private val b = 1.0 - 4.0 * a // 0.28466892
private val c = 0.5 - a * ln(4.0 * a) // 0.55991073
}
override fun EOTF(x: Double): Double = when (x) {
in 0.0..1.0 / 2.0 -> 3.0 * square(x)
in 1.0 / 2.0..1.0 -> (exp((x - c) / a) + b) / 12.0
else -> Double.NaN
}
override fun OETF(x: Double): Double = when (x) {
in 0.0..1.0 / 12.0 -> sqrt(3.0 * x)
in 1.0 / 12.0..1.0 -> a * ln(12.0 * x - b) + c
else -> Double.NaN
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
import kotlin.math.pow
/**
* [Rec. 2100](https://www.itu.int/rec/R-REC-BT.2100)
*/
class PQTransferFunction : TransferFunction {
companion object {
private val m_1 = 2610.0 / 16384.0 // 0.1593017578125
private val m_2 = 2523.0 / 4096.0 * 128.0 // 78.84375
private val c_1 = 3424.0 / 4096.0 // 0.8359375 = c_3 c_2 + 1
private val c_2 = 2413.0 / 4096.0 * 32.0 // 18.8515625
private val c_3 = 2392.0 / 4096.0 * 32.0 // 18.6875
}
override fun EOTF(x: Double): Double =
10000.0 * ((x.pow(1.0 / m_2).coerceAtLeast(0.0)) / (c_2 - c_3 * x.pow(1.0 / m_2))).pow(1.0 / m_1)
override fun OETF(x: Double): Double = ((c_1 + c_2 * x / 10000.0) / (1 + c_3 * x / 10000.0)).pow(
m_2
)
}

View File

@ -0,0 +1,16 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.rgb.transferfunction
interface TransferFunction {
// nonlinear -> linear
fun EOTF(x: Double): Double
// linear -> nonlinear
fun OETF(x: Double): Double
}

View File

@ -0,0 +1,74 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.zcam
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.util.Matrix3
import kotlin.math.pow
data class Izazbz(
val Iz: Double,
val az: Double,
val bz: Double,
) {
fun toXyz(): CieXyz {
val (x_, y_, z) = lmsToXyz * (IzazbzToLms * doubleArrayOf(Iz + epsilon, az, bz)).map {
10000.0 * ((c_1 - it.pow(1.0 / rho)) / (c_3 * it.pow(1.0 / rho) - c_2)).pow(1.0 / eta)
}.toDoubleArray()
val x = (x_ + (b - 1.0) * z) / b
val y = (y_ + (g - 1.0) * x) / g
return CieXyz(
x = x,
y = y,
z = z,
)
}
companion object {
private const val b = 1.15
private const val g = 0.66
private const val c_1 = 3424.0 / 4096.0
private const val c_2 = 2413.0 / 128.0
private const val c_3 = 2392.0 / 128.0
private const val eta = 2610.0 / 16384.0
private const val rho = 1.7 * 2523.0 / 32.0
private const val epsilon = 3.7035226210190005E-11
private val xyzToLms: Matrix3 = Matrix3(
doubleArrayOf(0.41478972, 0.579999, 0.01464800),
doubleArrayOf(-0.2015100, 1.120649, 0.05310080),
doubleArrayOf(-0.0166008, 0.264800, 0.66847990),
)
private val lmsToXyz: Matrix3 = xyzToLms.inverse()
private val lmsToIzazbz: Matrix3 = Matrix3(
doubleArrayOf(0.0, 1.0, 0.0),
doubleArrayOf(3.524000, -4.066708, 0.542708),
doubleArrayOf(0.199076, 1.096799, -1.295875),
)
private val IzazbzToLms: Matrix3 = lmsToIzazbz.inverse()
fun CieXyz.toIzazbz(): Izazbz {
val (I, az, bz) = lmsToIzazbz * (
xyzToLms * doubleArrayOf(
b * x - (b - 1.0) * z,
g * y - (g - 1.0) * x,
z,
)
).map {
((c_1 + c_2 * (it / 10000.0).pow(eta)) / (1.0 + c_3 * (it / 10000.0).pow(eta))).pow(
rho
)
}.toDoubleArray()
return Izazbz(
Iz = I - epsilon,
az = az,
bz = bz,
)
}
}
}

View File

@ -0,0 +1,162 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.colorspace.zcam
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb.Companion.toRgb
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz.Companion.toIzazbz
import me.ash.reader.ui.theme.palette.util.square
import me.ash.reader.ui.theme.palette.util.toDegrees
import me.ash.reader.ui.theme.palette.util.toRadians
import kotlin.math.*
data class Zcam(
val hz: Double = Double.NaN,
val Qz: Double = Double.NaN,
val Jz: Double = Double.NaN,
val Mz: Double = Double.NaN,
val Cz: Double = Double.NaN,
val Sz: Double = Double.NaN,
val Vz: Double = Double.NaN,
val Kz: Double = Double.NaN,
val Wz: Double = Double.NaN,
val cond: ViewingConditions,
) {
fun toIzazbz(): Izazbz {
require(!hz.isNaN()) { "Must provide hz." }
require(!Qz.isNaN() || !Jz.isNaN()) { "Must provide Qz or Jz." }
require(!Mz.isNaN() || !Cz.isNaN() || !Sz.isNaN() || !Vz.isNaN() || !Kz.isNaN() || !Wz.isNaN()) {
"Must provide Mz, Cz, Sz, Vz, Kz or Wz."
}
with(cond) {
val Iz = (
when {
!Qz.isNaN() -> Qz
!Jz.isNaN() -> Jz * Qzw / 100.0
else -> Double.NaN
} / (2700.0 * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2))
).pow(F_b.pow(0.12) / (1.6 * F_s))
val Jz = Jz.takeUnless { it.isNaN() } ?: when {
!Qz.isNaN() -> 100.0 * Qz / Qzw
else -> Double.NaN
}
val Qz = Qz.takeUnless { it.isNaN() } ?: when {
!Jz.isNaN() -> Jz * Qzw / 100.0
else -> Double.NaN
}
val Cz = Cz.takeUnless { it.isNaN() } ?: when {
!Sz.isNaN() -> Qz * square(Sz) / (100.0 * Qzw * F_L.pow(1.2))
!Vz.isNaN() -> sqrt((square(Vz) - square(Jz - 58.0)) / 3.4)
!Kz.isNaN() -> sqrt((square((100.0 - Kz) / 0.8) - square(Jz)) / 8.0)
!Wz.isNaN() -> sqrt(square(100.0 - Wz) - square(100.0 - Jz))
else -> Double.NaN
}
val Mz = Mz.takeUnless { it.isNaN() } ?: (Cz * Qzw / 100.0)
val ez = 1.015 + cos(89.038 + hz).toRadians()
val Cz_ =
(Mz * Izw.pow(0.78) * F_b.pow(0.1) / (100.0 * ez.pow(0.068) * F_L.pow(0.2))).pow(1.0 / (0.37 * 2.0))
val hzRad = hz.toRadians()
val az = Cz_ * cos(hzRad)
val bz = Cz_ * sin(hzRad)
return Izazbz(
Iz = Iz,
az = az,
bz = bz,
)
}
}
fun clampToRgb(colorSpace: RgbColorSpace): Rgb = toIzazbz()
.toXyz()
.toRgb(cond.luminance, colorSpace)
.takeIf { it.isInGamut() }
?: copy(Cz = findChromaBoundaryInRgb(colorSpace, 0.001))
.toIzazbz()
.toXyz()
.toRgb(cond.luminance, colorSpace)
.clamp()
private fun findChromaBoundaryInRgb(
colorSpace: RgbColorSpace,
error: Double
): Double = chromaBoundary.getOrPut(Triple(colorSpace.hashCode(), hz, Jz)) {
var low = 0.0
var high = Cz
var current = this
while (high - low >= error) {
val mid = (low + high) / 2.0
current = copy(Cz = mid)
if (!current.toIzazbz().toXyz().toRgb(cond.luminance, colorSpace).isInGamut()) {
high = mid
} else {
val next = current.copy(Cz = mid + error).toIzazbz().toXyz().toRgb(cond.luminance, colorSpace)
if (next.isInGamut()) {
low = mid
} else {
break
}
}
}
current.Cz
}
companion object {
private val chromaBoundary: MutableMap<Triple<Int, Double, Double>, Double> = mutableMapOf()
data class ViewingConditions(
val whitePoint: CieXyz,
val luminance: Double,
val F_s: Double,
val L_a: Double,
val Y_b: Double,
) {
private val absoluteWhitePoint = whitePoint * luminance
private val Y_w = absoluteWhitePoint.luminance
val F_b = sqrt(Y_b / Y_w)
val F_L = 0.171 * L_a.pow(1.0 / 3.0) * (1 - exp(-48.0 / 9.0 * L_a))
val Izw = absoluteWhitePoint.toIzazbz().Iz
val Qzw = 2700.0 * Izw.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2)
}
fun Izazbz.toZcam(cond: ViewingConditions): Zcam {
with(cond) {
val hz = atan2(bz, az).toDegrees().mod(360.0) // hue angle
val Qz =
2700.0 * Iz.pow(1.6 * F_s / F_b.pow(0.12)) * F_s.pow(2.2) * sqrt(F_b) * F_L.pow(0.2) // brightness
val Jz = 100.0 * Qz / Qzw // lightness
val ez = 1.015 + cos(89.038 + hz).toRadians() // ~ eccentricity factor
val Mz =
100.0 * (square(az) + square(bz)).pow(0.37) * ez.pow(0.068) * F_L.pow(0.2) /
(F_b.pow(0.1) * Izw.pow(0.78)) // colorfulness
val Cz = 100.0 * Mz / Qzw // chroma
val Sz = 100.0 * F_L.pow(0.6) * sqrt(Mz / Qz) // saturation
val Vz = sqrt(square(Jz - 58.0) + 3.4 * square(Cz)) // vividness
val Kz = 100.0 - 0.8 * sqrt(square(Jz) + 8.0 * square(Cz)) // blackness
val Wz = 100.0 - sqrt(square(100.0 - Jz) + square(Cz)) // blackness
return Zcam(
hz = hz,
Qz = Qz,
Jz = Jz,
Mz = Mz,
Cz = Cz,
Sz = Sz,
Vz = Vz,
Kz = Kz,
Wz = Wz,
cond = cond,
)
}
}
}
}

View File

@ -0,0 +1,97 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.core
import androidx.compose.runtime.Composable
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab.Companion.toCieLab
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.colorspace.oklab.Oklch
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb.Companion.toRgb
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz
import me.ash.reader.ui.theme.palette.colorspace.zcam.Izazbz.Companion.toIzazbz
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam.Companion.toZcam
@Composable
fun rgb(
r: Double,
g: Double,
b: Double,
): Rgb = Rgb(
r = r,
g = g,
b = b,
colorSpace = LocalRgbColorSpace.current,
)
@Composable
fun zcamLch(
L: Double,
C: Double,
h: Double,
): Zcam = Zcam(
hz = h,
Jz = L,
Cz = C,
cond = LocalZcamViewingConditions.current,
)
@Composable
fun zcam(
hue: Double = Double.NaN,
brightness: Double = Double.NaN,
lightness: Double = Double.NaN,
colorfulness: Double = Double.NaN,
chroma: Double = Double.NaN,
saturation: Double = Double.NaN,
vividness: Double = Double.NaN,
blackness: Double = Double.NaN,
whiteness: Double = Double.NaN,
): Zcam = Zcam(
hz = hue,
Qz = brightness,
Jz = lightness,
Mz = colorfulness,
Cz = chroma,
Sz = saturation,
Vz = vividness,
Kz = blackness,
Wz = whiteness,
cond = LocalZcamViewingConditions.current,
)
@Composable
fun CieXyz.toRgb(): Rgb = toRgb(LocalLuminance.current, LocalRgbColorSpace.current)
@Composable
fun CieLab.toXyz(): CieXyz = toXyz(LocalWhitePoint.current, LocalLuminance.current)
@Composable
fun CieXyz.toCieLab(): CieLab = toCieLab(LocalWhitePoint.current, LocalLuminance.current)
@Composable
fun Rgb.toXyz(): CieXyz = toXyz(LocalLuminance.current)
@Composable
fun Rgb.toZcam(): Zcam = toXyz().toIzazbz().toZcam()
@Composable
fun Oklch.clampToRgb(): Rgb = clampToRgb(LocalRgbColorSpace.current)
@Composable
fun Izazbz.toZcam(): Zcam = toZcam(LocalZcamViewingConditions.current)
@Composable
fun Zcam.toRgb(): Rgb =
toIzazbz().toXyz()
.toRgb(LocalZcamViewingConditions.current.luminance, LocalRgbColorSpace.current)
@Composable
fun Zcam.clampToRgb(): Rgb = clampToRgb(LocalRgbColorSpace.current)

View File

@ -0,0 +1,67 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.core
import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import me.ash.reader.ui.theme.palette.colorspace.rgb.Rgb
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
fun Rgb.toColor(): Color = if (!r.isNaN() && !g.isNaN() && !b.isNaN())
Color(
red = r.toFloat(),
green = g.toFloat(),
blue = b.toFloat(),
colorSpace = when (colorSpace) {
RgbColorSpace.Srgb -> ColorSpaces.Srgb
RgbColorSpace.DisplayP3 -> ColorSpaces.DisplayP3
RgbColorSpace.BT2020 -> ColorSpaces.Bt2020
else -> ColorSpaces.Srgb
}
) else Color.Black
@Composable
fun Color.toRgb(): Rgb {
val color = convert(
when (LocalRgbColorSpace.current) {
RgbColorSpace.Srgb -> ColorSpaces.Srgb
RgbColorSpace.DisplayP3 -> ColorSpaces.DisplayP3
RgbColorSpace.BT2020 -> ColorSpaces.Bt2020
else -> ColorSpaces.Srgb
}
)
return Rgb(
r = color.red.toDouble(),
g = color.green.toDouble(),
b = color.blue.toDouble(),
colorSpace = LocalRgbColorSpace.current
)
}
@Composable
fun animateZcamLchAsState(
targetValue: ZcamLch,
animationSpec: AnimationSpec<ZcamLch> = spring(),
finishedListener: ((ZcamLch) -> Unit)? = null
): State<ZcamLch> {
val converter = remember {
TwoWayConverter<ZcamLch, AnimationVector3D>(
convertToVector = {
AnimationVector3D(it.L.toFloat(), it.C.toFloat(), it.h.toFloat())
},
convertFromVector = {
ZcamLch(L = it.v1.toDouble(), C = it.v2.toDouble(), h = it.v3.toDouble())
}
)
}
return animateValueAsState(targetValue, converter, animationSpec, finishedListener = finishedListener)
}

View File

@ -0,0 +1,65 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.core
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
import me.ash.reader.ui.theme.palette.colorspace.rgb.RgbColorSpace
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
import me.ash.reader.ui.theme.palette.data.Illuminant
val LocalWhitePoint = staticCompositionLocalOf {
Illuminant.D65
}
val LocalLuminance = staticCompositionLocalOf {
1.0
}
val LocalRgbColorSpace = staticCompositionLocalOf {
RgbColorSpace.Srgb
}
val LocalZcamViewingConditions = staticCompositionLocalOf {
createZcamViewingConditions()
}
@Composable
fun ProvideZcamViewingConditions(
whitePoint: CieXyz = Illuminant.D65,
luminance: Double = 203.0, // BT.2408-4, HDR white luminance
surroundFactor: Double = 0.69, // average surround
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalWhitePoint provides whitePoint,
LocalLuminance provides luminance,
LocalZcamViewingConditions provides createZcamViewingConditions(
whitePoint = whitePoint,
luminance = luminance,
surroundFactor = surroundFactor,
)
) {
content()
}
}
fun createZcamViewingConditions(
whitePoint: CieXyz = Illuminant.D65,
luminance: Double = 203.0,
surroundFactor: Double = 0.69,
): Zcam.Companion.ViewingConditions = Zcam.Companion.ViewingConditions(
whitePoint = whitePoint,
luminance = luminance,
F_s = surroundFactor,
L_a = 0.4 * luminance,
Y_b = CieLab(50.0, 0.0, 0.0).toXyz(whitePoint, luminance).luminance,
)

View File

@ -0,0 +1,28 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.core
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
data class ZcamLch(
val L: Double,
val C: Double,
val h: Double,
) {
@Composable
fun toZcam(): Zcam = zcamLch(L = L, C = C, h = h)
companion object {
@Composable
fun Color.toZcamLch(): ZcamLch = toRgb().toZcam().toZcamLch()
fun Zcam.toZcamLch(): ZcamLch = ZcamLch(L = Jz, C = Cz, h = hz)
}
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.data
import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz
object Illuminant {
/** CIE Illuminant D65 - standard 2º observer. 6504 K color temperature.
* Values are calculated from [this table](https://github.com/gpmarques/colorimetry/blob/master/all_1nm_data.xls).
*/
val D65: CieXyz by lazy {
CieXyz(
x = 10043.7000153676 / 10567.0816669881,
y = 1.0,
z = 11505.7421788588 / 10567.0816669881,
)
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.data
import me.ash.reader.ui.theme.palette.TonalPalettes
data class Theme(
val hue: Double,
val primaryChroma: Double,
) {
fun toTonalPalettes(): TonalPalettes = TonalPalettes(
hue = hue,
primaryChroma = primaryChroma,
)
companion object {
fun TonalPalettes.toTheme(): Theme = Theme(
hue = hue,
primaryChroma = primaryChroma,
)
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
*/
package me.ash.reader.ui.theme.palette.dynamic
import me.ash.reader.ui.theme.palette.colorspace.zcam.Zcam
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.sign
fun Zcam.harmonizeTowards(
target: Zcam,
factor: Double = 0.5,
maxHueShift: Double = 15.0
): Zcam = copy(
hz = hz + (
((180.0 - abs(abs(hz - target.hz) - 180.0)) * factor).coerceAtMost(maxHueShift)
) * (
listOf(
target.hz - hz,
target.hz - hz + 360.0,
target.hz - hz - 360.0
).minOf {
it.absoluteValue
}.sign.takeIf { it != 0.0 } ?: 1.0
)
)

View File

@ -0,0 +1,52 @@
/**
* Copyright (C) 2021 Kyant0
*
* @link https://github.com/Kyant0/MusicYou
* @author Kyant0
* @modifier Ashinch
*/
package me.ash.reader.ui.theme.palette.dynamic
import android.annotation.SuppressLint
import android.app.WallpaperManager
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import kotlinx.coroutines.flow.map
import me.ash.reader.ui.ext.DataStoreKeys
import me.ash.reader.ui.ext.dataStore
import me.ash.reader.ui.theme.palette.TonalPalettes
import me.ash.reader.ui.theme.palette.TonalPalettes.Companion.toTonalPalettes
import me.ash.reader.ui.theme.palette.safeHexToColor
@SuppressLint("FlowOperatorInvokedInComposition")
@Composable
fun extractTonalPalettesFromUserWallpaper(): List<TonalPalettes> {
val context = LocalContext.current
val customPrimaryColor =
context.dataStore.data.map { it[DataStoreKeys.CustomPrimaryColor.key] ?: "" }
.collectAsState(initial = "").value
val preset = mutableListOf(
Color(0xFF80BBFF).toTonalPalettes(),
Color(0xFFFFD8E4).toTonalPalettes(),
Color(0xFF62539f).toTonalPalettes(),
Color(0xFFE9B666).toTonalPalettes(),
customPrimaryColor.safeHexToColor().toTonalPalettes()
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && !LocalView.current.isInEditMode) {
val colors = WallpaperManager.getInstance(LocalContext.current)
.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
val primary = colors?.primaryColor?.toArgb()
val secondary = colors?.secondaryColor?.toArgb()
val tertiary = colors?.tertiaryColor?.toArgb()
if (primary != null) preset.add(Color(primary).toTonalPalettes())
if (secondary != null) preset.add(Color(secondary).toTonalPalettes())
if (tertiary != null) preset.add(Color(tertiary).toTonalPalettes())
}
return preset
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.theme.color
package me.ash.reader.ui.theme.palette.preset
import androidx.compose.ui.graphics.Color

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.theme.color
package me.ash.reader.ui.theme.palette.preset
import androidx.compose.ui.graphics.Color

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.theme.color
package me.ash.reader.ui.theme.palette.preset
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.theme.color
package me.ash.reader.ui.theme.palette.preset
import androidx.compose.ui.graphics.Color

View File

@ -0,0 +1,64 @@
package me.ash.reader.ui.theme.palette.util
import kotlin.math.PI
internal fun square(x: Double): Double = x * x
internal fun Double.toRadians(): Double = this * PI / 180.0
internal fun Double.toDegrees(): Double = this * 180.0 / PI
operator fun DoubleArray.times(x: Double): DoubleArray = map { it * x }.toDoubleArray()
operator fun DoubleArray.div(x: Double): DoubleArray = map { it / x }.toDoubleArray()
class Matrix3(
private val x: DoubleArray,
private val y: DoubleArray,
private val z: DoubleArray,
) {
fun inverse(): Matrix3 {
val det = determinant()
return Matrix3(
doubleArrayOf(
(y[1] * z[2] - y[2] * z[1]) / det,
(y[2] * z[0] - y[0] * z[2]) / det,
(y[0] * z[1] - y[1] * z[0]) / det,
),
doubleArrayOf(
(x[2] * z[1] - x[1] * z[2]) / det,
(x[0] * z[2] - x[2] * z[0]) / det,
(x[1] * z[0] - x[0] * z[1]) / det,
),
doubleArrayOf(
(x[1] * y[2] - x[2] * y[1]) / det,
(x[2] * y[0] - x[0] * y[2]) / det,
(x[0] * y[1] - x[1] * y[0]) / det,
),
).transpose()
}
private fun determinant(): Double =
x[0] * (y[1] * z[2] - y[2] * z[1]) -
x[1] * (y[0] * z[2] - y[2] * z[0]) +
x[2] * (y[0] * z[1] - y[1] * z[0])
private fun transpose() = Matrix3(
doubleArrayOf(x[0], y[0], z[0]),
doubleArrayOf(x[1], y[1], z[1]),
doubleArrayOf(x[2], y[2], z[2]),
)
operator fun get(i: Int): DoubleArray = when (i) {
0 -> x
1 -> y
2 -> z
else -> throw IndexOutOfBoundsException("Index must be 0, 1 or 2")
}
operator fun times(vec: DoubleArray): DoubleArray = doubleArrayOf(
x[0] * vec[0] + x[1] * vec[1] + x[2] * vec[2],
y[0] * vec[0] + y[1] * vec[1] + y[2] * vec[2],
z[0] * vec[0] + z[1] * vec[1] + z[2] * vec[2],
)
override fun toString(): String = "{" + arrayOf(x, y, z).joinToString { "[" + it.joinToString() + "]" } + "}"
}

View File

@ -0,0 +1,159 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="888dp"
android:height="677.20703dp"
android:viewportWidth="888"
android:viewportHeight="677.20703">
<path
android:pathData="M307.693,659.535l8.724,2.613l14.231,-32.408l-12.876,-3.856l-10.079,33.651z"
android:fillColor="#ffb8b8"/>
<path
android:pathData="M306.32,656.02l17.182,5.146 0.001,0a11.431,11.431 0,0 1,7.669 14.229l-0.107,0.356 -28.131,-8.426Z"
android:fillColor="#2f2e41"/>
<path
android:pathData="M390.602,666.663l8.551,-3.135l-8.023,-34.473l-12.62,4.627l12.092,32.981z"
android:fillColor="#ffb8b8"/>
<path
android:pathData="M387.398,664.671l16.839,-6.174 0.001,-0a11.431,11.431 0,0 1,14.666 6.797l0.128,0.349L391.46,675.751Z"
android:fillColor="#2f2e41"/>
<path
android:pathData="M322.321,600.868l-14.857,49.029l17.828,5.2l17.086,-45.315l-20.057,-8.914z"
android:fillColor="#2f2e41"/>
<path
android:pathData="M369.121,612.011l13.372,46.057l18.571,-8.914l-13.371,-43.086l-18.572,5.943z"
android:fillColor="#2f2e41"/>
<path
android:pathData="M362.162,620.318a203.98,203.98 0,0 1,-35.458 -3.27l-0.298,-0.06v-11.33l-6.79,-0.754 5.3,-18.928c-2.47,-29.16 1.04,-66.089 2.176,-76.719 0.26,-2.493 0.431,-3.891 0.431,-3.891l5.959,-50.65 9.97,-9.203 4.529,2.948 8.402,8.401c9.766,24.034 17.515,46.661 17.564,48.115l32.751,104.953 -0.221,0.156C395.106,618.113 377.77,620.318 362.162,620.318Z"
android:fillColor="#3f3d56"/>
<path
android:fillColor="#FF000000"
android:pathData="M337.708,477.181l-2.29,14.294l15.503,6.395l-13.213,-20.689z"
android:strokeAlpha="0.2"
android:fillAlpha="0.2"/>
<path
android:pathData="M368.128,445.438L332.284,445.438a2.784,2.784 0,0 1,-2.781 -2.781v-15.45a20.703,20.703 0,0 1,41.406 0v15.45A2.784,2.784 0,0 1,368.128 445.438Z"
android:fillColor="#2f2e41"/>
<path
android:pathData="M354.164,429.328m-15.179,0a15.179,15.179 0,1 1,30.357 0a15.179,15.179 0,1 1,-30.357 0"
android:fillColor="#ffb8b8"/>
<path
android:pathData="M375.807,428.752L353.889,428.752l-0.225,-3.147 -1.124,3.147h-3.375l-0.445,-6.237 -2.227,6.237h-6.53v-0.309a16.395,16.395 0,0 1,16.377 -16.377L359.43,412.065a16.395,16.395 0,0 1,16.377 16.377Z"
android:fillColor="#2f2e41"/>
<path
android:pathData="M353.71,448.322a2.841,2.841 0,0 1,-0.492 -0.043l-16.049,-2.832L337.169,418.922h17.667l-0.437,0.51c-6.086,7.097 -1.501,18.606 1.774,24.834a2.74,2.74 0,0 1,-0.218 2.909A2.77,2.77 0,0 1,353.71 448.322Z"
android:fillColor="#2f2e41"/>
<path
android:pathData="M382.029,512.433h-8.797a1.131,1.131 0,0 1,-1.13 -1.025l-1.761,-18.049h14.579l-1.761,18.049A1.131,1.131 0,0 1,382.029 512.433Z"
android:fillColor="#6c63ff"/>
<path
android:pathData="M384.896,495.63L370.364,495.63a1.137,1.137 0,0 1,-1.135 -1.135v-2.725a1.137,1.137 0,0 1,1.135 -1.135h14.532a1.137,1.137 0,0 1,1.135 1.135v2.725A1.137,1.137 0,0 1,384.896 495.63Z"
android:fillColor="#2f2e41"/>
<path
android:fillColor="#FF000000"
android:pathData="M327.892,504.667l0,0a27.881,27.881 0,0 0,33.468 6.765l3.304,-1.635Z"
android:strokeAlpha="0.2"
android:fillAlpha="0.2"/>
<path
android:pathData="M380.711,501.173a6.966,6.966 0,0 0,-10.676 0.322l-15.326,-4.302 -4.886,8.676 21.728,5.772a7.004,7.004 0,0 0,9.16 -10.467Z"
android:fillColor="#ffb8b8"/>
<path
android:pathData="M344.139,510.801c-7.286,0.001 -17.145,-4.271 -28.849,-12.546a5.731,5.731 0,0 1,-2.413 -3.928c-0.863,-5.469 4.471,-12.863 4.995,-13.572l5.608,-15.401c0.065,-0.25 1.872,-6.914 6.409,-9.284a7.438,7.438 0,0 1,6.214 -0.265c8.642,3.147 1.894,27.448 0.968,30.635l11.45,5.389 7.271,4.635 9.958,1.042 -2.704,12.512 -15.123,0.34A15.434,15.434 0,0 1,344.139 510.801Z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M887.675,396.659A98.58,98.58 0,1 0,698.454 435.469c-0.096,-0.107 -0.196,-0.211 -0.292,-0.319a98.666,98.666 0,0 0,17.954 27.783c0.022,0.025 0.045,0.049 0.068,0.073 0.606,0.66 1.215,1.317 1.838,1.96a98.28,98.28 0,0 0,69.529 30.254l-3.331,180.929h10.291l-2.083,-119.415 14.887,-7.838 -2.271,-4.314 -12.711,6.692 -0.978,-56.064A98.578,98.578 0,0 0,887.675 396.659Z"
android:fillColor="#e6e6e6"/>
<path
android:pathData="M595.087,348.612a115.526,115.526 0,1 0,-221.75 45.481c-0.113,-0.126 -0.23,-0.248 -0.342,-0.374a115.628,115.628 0,0 0,21.041 32.559c0.026,0.029 0.053,0.057 0.08,0.086 0.71,0.774 1.423,1.543 2.154,2.297a115.176,115.176 0,0 0,81.482 35.454l-3.904,212.033h12.06l-2.441,-139.944 17.446,-9.185 -2.661,-5.055 -14.896,7.842 -1.146,-65.702A115.525,115.525 0,0 0,595.087 348.612Z"
android:fillColor="#e6e6e6"/>
<path
android:pathData="M263.259,303.418A131.467,131.467 0,1 0,10.912 355.175c-0.129,-0.143 -0.261,-0.282 -0.389,-0.426a131.582,131.582 0,0 0,23.944 37.051c0.03,0.033 0.061,0.065 0.091,0.098 0.808,0.88 1.62,1.756 2.451,2.614a131.068,131.068 0,0 0,92.725 40.347L125.291,676.148h13.725L136.238,516.895 156.091,506.442l-3.029,-5.753 -16.952,8.925 -1.304,-74.767A131.465,131.465 0,0 0,263.259 303.418Z"
android:fillColor="#e6e6e6"/>
<path
android:pathData="M756.685,85.976m-85.976,0a85.976,85.976 0,1 1,171.952 0a85.976,85.976 0,1 1,-171.952 0"
android:fillColor="#ff6584"/>
<path
android:pathData="M245.559,187.616m-172.312,0a172.312,172.312 0,1 1,344.623 0a172.312,172.312 0,1 1,-344.623 0"
android:fillColor="#6c63ff"/>
<path
android:fillColor="#FF000000"
android:pathData="M118.329,72.526A172.325,172.325 0,0 0,405.452 254.895,172.327 172.327,0 1,1 118.329,72.526Z"
android:strokeAlpha="0.2"
android:fillAlpha="0.2"/>
<path
android:pathData="M246.032,187.616l0.474,0l8.521,488.532l-17.989,0l8.994,-488.532z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M244.954,461.173l28.484,-14.996l3.97,7.54l-28.484,14.996z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M509.115,671.578s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M505.514,652.803m-6.379,0a6.379,6.379 0,1 1,12.757 0a6.379,6.379 0,1 1,-12.757 0"
android:fillColor="#6c63ff"/>
<path
android:pathData="M504.476,663.545h1.801v12.604h-1.801z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M67.083,669.778s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M63.482,651.003m-6.379,0a6.379,6.379 0,1 1,12.757 0a6.379,6.379 0,1 1,-12.757 0"
android:fillColor="#6c63ff"/>
<path
android:pathData="M62.444,661.744h1.801v12.604h-1.801z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M171.514,670.678s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M167.913,651.903m-6.379,0a6.379,6.379 0,1 1,12.757 0a6.379,6.379 0,1 1,-12.757 0"
android:fillColor="#6c63ff"/>
<path
android:pathData="M166.875,662.644h1.801v12.604h-1.801z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M449.243,83.299l12.795,-10.233c-9.94,-1.097 -14.024,4.324 -15.695,8.615 -7.765,-3.224 -16.219,1.001 -16.219,1.001l25.6,9.294A19.372,19.372 0,0 0,449.243 83.299Z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M643.827,187.54l12.795,-10.233c-9.94,-1.097 -14.024,4.324 -15.695,8.615 -7.765,-3.224 -16.219,1.001 -16.219,1.001l25.6,9.294A19.372,19.372 0,0 0,643.827 187.54Z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M433.955,276.492l12.795,-10.233c-9.94,-1.097 -14.024,4.324 -15.695,8.615 -7.765,-3.224 -16.219,1.001 -16.219,1.001l25.6,9.294A19.372,19.372 0,0 0,433.955 276.492Z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M683.655,676.307s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M563.919,676.307s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M127.289,676.307s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M737.671,676.307s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M712.464,676.307s0.622,-13.027 13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M660.465,676.307s-0.622,-13.027 -13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M453.403,676.307s-0.622,-13.027 -13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M281.452,676.307s-0.622,-13.027 -13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M98.697,676.307s-0.622,-13.027 -13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M791.904,676.307s-0.622,-13.027 -13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M714.481,677.207s-0.622,-13.027 -13.366,-11.513"
android:fillColor="#3f3d56"/>
<path
android:pathData="M0,674.604h888v2h-888z"
android:fillColor="#3f3d56"/>
</vector>

View File

@ -79,7 +79,7 @@
<string name="color_and_style">颜色和样式</string>
<string name="color_and_style_desc">主题、色彩系统、字体大小</string>
<string name="interaction">交互</string>
<string name="interaction_desc">布局、触反馈</string>
<string name="interaction_desc">布局、触反馈</string>
<string name="languages">语言</string>
<string name="languages_desc">英语、中文</string>
<string name="tips_and_support">提示和支持</string>
@ -89,4 +89,16 @@
<string name="view_terms">查看《&lt;u&gt;服务条款与隐私政策&lt;/u&gt;</string>
<string name="terms_link">https://gitee.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY-zh.md</string>
<string name="agree_and_continue">同意并继续</string>
<string name="wallpaper_colors">壁纸颜色</string>
<string name="no_palettes">暂无色板</string>
<string name="only_android_8.1_plus">仅限 Android 8.1+</string>
<string name="basic_colors">基本颜色</string>
<string name="style">样式</string>
<string name="dark_theme">深色模式</string>
<string name="use_device_theme">跟随系统设置</string>
<string name="tonal_elevation">色调海拔</string>
<string name="fonts">字体</string>
<string name="basic_fonts">基本字体</string>
<string name="reading_fonts">阅读字体</string>
<string name="reading_fonts_size">阅读字体大小</string>
</resources>

View File

@ -89,4 +89,16 @@
<string name="view_terms">View the &lt;i&gt;&lt;u&gt;Terms of Service and Privacy Policy&lt;/u&gt;&lt;/i&gt;</string>
<string name="terms_link">https://github.com/Ashinch/ReadYou/blob/main/TERMS_OF_SERVICE_AND_PRIVACY_POLICY.md</string>
<string name="agree_and_continue">Agree and Continue</string>
<string name="wallpaper_colors">Wallpaper Colors</string>
<string name="no_palettes">No Palettes</string>
<string name="only_android_8.1_plus">Only Android 8.1+</string>
<string name="basic_colors">Basic Colors</string>
<string name="style">Style</string>
<string name="dark_theme">Dark Theme</string>
<string name="use_device_theme">Use Device Theme</string>
<string name="tonal_elevation">Tonal Elevation</string>
<string name="fonts">Fonts</string>
<string name="basic_fonts">Basic Fonts</string>
<string name="reading_fonts">Reading Fonts</string>
<string name="reading_fonts_size">Reading Fonts Size</string>
</resources>