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() { override fun onCreate() {
super.onCreate() super.onCreate()
CrashHandler(this) CrashHandler(this)
dataStoreInit()
applicationScope.launch(dispatcherDefault) { applicationScope.launch(dispatcherDefault) {
accountInit() accountInit()
dataStoreInit()
workerInit() workerInit()
} }
} }

View File

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

View File

@ -19,10 +19,12 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.ash.reader.ui.theme.palette.onDark
@Composable @Composable
fun Banner( fun Banner(
@ -33,20 +35,18 @@ fun Banner(
action: (@Composable () -> Unit)? = null, action: (@Composable () -> Unit)? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val lightThemeColors = MaterialTheme.colorScheme
val lightPrimaryContainer = lightThemeColors.primaryContainer
val lightOnSurface = lightThemeColors.onSurface
Surface( Surface(
modifier = modifier.fillMaxWidth().height(88.dp), modifier = modifier
color = MaterialTheme.colorScheme.surface, .fillMaxWidth()
.height(88.dp),
color = Color.Unspecified,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background(lightPrimaryContainer) .background(MaterialTheme.colorScheme.primaryContainer onDark MaterialTheme.colorScheme.onPrimaryContainer)
.clickable { onClick() } .clickable { onClick() }
.padding(16.dp, 20.dp), .padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -57,26 +57,29 @@ fun Banner(
imageVector = it, imageVector = it,
contentDescription = null, contentDescription = null,
modifier = Modifier.padding(end = 16.dp), modifier = Modifier.padding(end = 16.dp),
tint = lightOnSurface, tint = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
) )
} }
} }
Column( Column(
modifier = Modifier.weight(1f).fillMaxHeight(), modifier = Modifier
.weight(1f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Text(
text = title, text = title,
maxLines = if (desc == null) 2 else 1, maxLines = if (desc == null) 2 else 1,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp), style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
color = lightOnSurface, color = MaterialTheme.colorScheme.onSurface onDark MaterialTheme.colorScheme.surface,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
desc?.let { desc?.let {
Text( Text(
text = it, text = it,
style = MaterialTheme.typography.bodyMedium, 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, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
@ -84,9 +87,12 @@ fun Banner(
} }
action?.let { action?.let {
Box(Modifier.padding(start = 16.dp)) { Box(Modifier.padding(start = 16.dp)) {
CompositionLocalProvider(LocalContentColor provides lightOnSurface) { CompositionLocalProvider(
it() 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)!! get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
val Context.currentAccountType: Int val Context.currentAccountType: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!! 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) { suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
this.edit { 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") @Suppress("UNCHECKED_CAST")
fun <T> DataStore<Preferences>.get(dataStoreKeys: DataStoreKeys<T>): T? { fun <T> DataStore<Preferences>.get(dataStoreKeys: DataStoreKeys<T>): T? {
return runBlocking { return runBlocking {
@ -59,4 +71,14 @@ sealed class DataStoreKeys<T> {
override val key: Preferences.Key<Int> override val key: Preferences.Key<Int>
get() = intPreferencesKey("currentAccountType") 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 androidx.compose.ui.platform.LocalContext
import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsHeight 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.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.ui.ext.animatedComposable import me.ash.reader.ui.ext.animatedComposable
import me.ash.reader.ui.ext.isFirstLaunch import me.ash.reader.ui.ext.isFirstLaunch
import me.ash.reader.ui.page.home.HomePage 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.settings.SettingsPage
import me.ash.reader.ui.page.startup.StartupPage import me.ash.reader.ui.page.startup.StartupPage
import me.ash.reader.ui.theme.AppTheme import me.ash.reader.ui.theme.AppTheme
@ -38,11 +38,11 @@ fun HomeEntry() {
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme()) setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme()) setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
} }
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { Column {
Row( Row(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.statusBarsPadding() .background(MaterialTheme.colorScheme.surface),
) { ) {
AnimatedNavHost( AnimatedNavHost(
navController = navController, navController = navController,
@ -57,12 +57,16 @@ fun HomeEntry() {
animatedComposable(route = RouteName.SETTINGS) { animatedComposable(route = RouteName.SETTINGS) {
SettingsPage(navController) SettingsPage(navController)
} }
animatedComposable(route = RouteName.COLOR_AND_STYLE) {
ColorAndStyle(navController)
}
} }
} }
Spacer( Spacer(
modifier = Modifier modifier = Modifier
.navigationBarsHeight() .navigationBarsHeight()
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
) )
} }
} }

View File

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

View File

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

View File

@ -1,13 +1,16 @@
package me.ash.reader.ui.page.home package me.ash.reader.ui.page.home
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.ui.component.ViewPager import me.ash.reader.ui.component.ViewPager
@ -91,7 +94,11 @@ fun HomePage(
) )
} }
Column { Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.statusBarsPadding(),
) {
ViewPager( ViewPager(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
state = viewState.pagerState, 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 * @modifier Ashinch
*/ */
package me.ash.reader.ui.component package me.ash.reader.ui.page.settings
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -22,6 +22,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -30,6 +31,7 @@ import androidx.compose.ui.unit.sp
@Composable @Composable
fun SelectableSettingGroupItem( fun SelectableSettingGroupItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enable: Boolean = true,
selected: Boolean = false, selected: Boolean = false,
title: String, title: String,
desc: String? = null, desc: String? = null,
@ -37,7 +39,7 @@ fun SelectableSettingGroupItem(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Surface( Surface(
modifier = modifier.clickable { onClick() }, modifier = modifier.clickable { onClick() }.alpha(if (enable) 1f else 0.5f),
color = Color.Unspecified, color = Color.Unspecified,
) { ) {
Row( Row(
@ -45,7 +47,7 @@ fun SelectableSettingGroupItem(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.background( .background(
color = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.surface, color = if (selected) MaterialTheme.colorScheme.onSurface else Color.Unspecified,
shape = RoundedCornerShape(24.dp) shape = RoundedCornerShape(24.dp)
) )
.padding(8.dp, 16.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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.component.Banner import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton 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.page.common.RouteName
import me.ash.reader.ui.theme.palette.onLight
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -27,9 +28,13 @@ fun SettingsPage(
navController: NavHostController, navController: NavHostController,
) { ) {
Scaffold( 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 = { topBar = {
SmallTopAppBar( SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface),
title = {}, title = {},
navigationIcon = { navigationIcon = {
FeedbackIconButton( FeedbackIconButton(
@ -67,6 +72,7 @@ fun SettingsPage(
title = stringResource(R.string.accounts), title = stringResource(R.string.accounts),
desc = stringResource(R.string.accounts_desc), desc = stringResource(R.string.accounts_desc),
icon = Icons.Outlined.AccountCircle, icon = Icons.Outlined.AccountCircle,
enable = false,
) {} ) {}
} }
item { item {
@ -74,13 +80,16 @@ fun SettingsPage(
title = stringResource(R.string.color_and_style), title = stringResource(R.string.color_and_style),
desc = stringResource(R.string.color_and_style_desc), desc = stringResource(R.string.color_and_style_desc),
icon = Icons.Outlined.Palette, icon = Icons.Outlined.Palette,
) {} ) {
navController.navigate(RouteName.COLOR_AND_STYLE)
}
} }
item { item {
SelectableSettingGroupItem( SelectableSettingGroupItem(
title = stringResource(R.string.interaction), title = stringResource(R.string.interaction),
desc = stringResource(R.string.interaction_desc), desc = stringResource(R.string.interaction_desc),
icon = Icons.Outlined.TouchApp, icon = Icons.Outlined.TouchApp,
enable = false,
) {} ) {}
} }
item { item {
@ -88,6 +97,7 @@ fun SettingsPage(
title = stringResource(R.string.languages), title = stringResource(R.string.languages),
desc = stringResource(R.string.languages_desc), desc = stringResource(R.string.languages_desc),
icon = Icons.Outlined.Language, icon = Icons.Outlined.Language,
enable = false,
) {} ) {}
} }
item { item {
@ -95,6 +105,7 @@ fun SettingsPage(
title = stringResource(R.string.tips_and_support), title = stringResource(R.string.tips_and_support),
desc = stringResource(R.string.tips_and_support_desc), desc = stringResource(R.string.tips_and_support_desc),
icon = Icons.Outlined.TipsAndUpdates, 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.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import com.ireward.htmlcompose.HtmlText import com.ireward.htmlcompose.HtmlText
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
@ -37,7 +37,7 @@ fun StartupPage(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Scaffold( Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.surface), modifier = Modifier.statusBarsPadding().background(MaterialTheme.colorScheme.surface),
topBar = {}, topBar = {},
content = { content = {
LazyColumn { LazyColumn {
@ -87,13 +87,16 @@ fun StartupPage(
} }
}, },
bottomBar = { bottomBar = {
Row( // Row(
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.padding(24.dp), // .padding(24.dp),
horizontalArrangement = Arrangement.End, // horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically, // verticalAlignment = Alignment.CenterVertically,
) { // ) {
// }
},
floatingActionButton = {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { onClick = {
navController.navigate(route = RouteName.HOME) navController.navigate(route = RouteName.HOME)
@ -110,7 +113,6 @@ fun StartupPage(
text = { Text(text = stringResource(R.string.agree_and_continue)) }, text = { Text(text = stringResource(R.string.agree_and_continue)) },
) )
} }
}
) )
} }

View File

@ -1,51 +1,54 @@
package me.ash.reader.ui.theme package me.ash.reader.ui.theme
import android.os.Build import android.annotation.SuppressLint
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import me.ash.reader.ui.theme.color.PurpleColor import kotlinx.coroutines.flow.map
import me.ash.reader.ui.ext.DataStoreKeys
private val LightThemeColors = PurpleColor.lightColorScheme import me.ash.reader.ui.ext.dataStore
private val DarkThemeColors = PurpleColor.darkColorScheme import me.ash.reader.ui.theme.palette.LocalTonalPalettes
import me.ash.reader.ui.theme.palette.TonalPalettes
val LocalLightThemeColors = staticCompositionLocalOf { LightThemeColors } import me.ash.reader.ui.theme.palette.core.ProvideZcamViewingConditions
val LocalDarkThemeColors = staticCompositionLocalOf { DarkThemeColors } 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 @Composable
fun AppTheme( fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(), useDarkTheme: Boolean = isSystemInDarkTheme(),
wallpaperPalettes: List<TonalPalettes> = extractTonalPalettesFromUserWallpaper(),
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
// Dynamic color is available on Android 12+ val context = LocalContext.current
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val themeIndex = context.dataStore.data.map { it[DataStoreKeys.ThemeIndex.key] ?: 0 }
.collectAsState(initial = 0).value
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
}
ProvideZcamViewingConditions {
CompositionLocalProvider( CompositionLocalProvider(
LocalLightThemeColors provides light, LocalTonalPalettes provides wallpaperPalettes[
LocalDarkThemeColors provides dark, if (themeIndex >= wallpaperPalettes.size) {
when {
wallpaperPalettes.size == 5 -> 0
wallpaperPalettes.size > 5 -> 5
else -> 0
}
} else {
themeIndex
}
]
) { ) {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme =
if (useDarkTheme) dynamicDarkColorScheme()
else dynamicLightColorScheme(),
typography = AppTypography, typography = AppTypography,
content = content 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 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 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.ColorScheme
import androidx.compose.material3.darkColorScheme 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 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">颜色和样式</string>
<string name="color_and_style_desc">主题、色彩系统、字体大小</string> <string name="color_and_style_desc">主题、色彩系统、字体大小</string>
<string name="interaction">交互</string> <string name="interaction">交互</string>
<string name="interaction_desc">布局、触反馈</string> <string name="interaction_desc">布局、触反馈</string>
<string name="languages">语言</string> <string name="languages">语言</string>
<string name="languages_desc">英语、中文</string> <string name="languages_desc">英语、中文</string>
<string name="tips_and_support">提示和支持</string> <string name="tips_and_support">提示和支持</string>
@ -89,4 +89,16 @@
<string name="view_terms">查看《&lt;u&gt;服务条款与隐私政策&lt;/u&gt;</string> <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="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="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> </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="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="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="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> </resources>