MindMachine: improve contrast and fix visual flash/pulse rendering
This commit is contained in:
@@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@@ -32,11 +33,15 @@ import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -49,21 +54,35 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.mindmachine.mvp.audio.BinauralAudioEngine
|
||||
import com.mindmachine.mvp.audio.HeadsetMonitor
|
||||
import com.mindmachine.mvp.data.SettingsRepository
|
||||
import com.mindmachine.mvp.domain.CountdownPreference
|
||||
import com.mindmachine.mvp.domain.RuntimeState
|
||||
import com.mindmachine.mvp.domain.SessionMode
|
||||
import com.mindmachine.mvp.domain.VisualPattern
|
||||
import com.mindmachine.mvp.session.MainViewModel
|
||||
import com.mindmachine.mvp.session.computeVisualAlpha
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val MindMachineColors = darkColorScheme(
|
||||
primary = Color(0xFF8AB4FF),
|
||||
onPrimary = Color(0xFF001B3D),
|
||||
primaryContainer = Color(0xFF004689),
|
||||
onPrimaryContainer = Color(0xFFD7E3FF),
|
||||
secondary = Color(0xFFFFD166),
|
||||
onSecondary = Color(0xFF2E2300),
|
||||
background = Color(0xFF05070D),
|
||||
onBackground = Color(0xFFF3F6FF),
|
||||
surface = Color(0xFF121621),
|
||||
onSurface = Color(0xFFF3F6FF),
|
||||
error = Color(0xFFFF6B6B),
|
||||
onError = Color(0xFF3A0002)
|
||||
)
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val vm: MainViewModel by viewModels {
|
||||
MainViewModel.Factory(SettingsRepository(applicationContext), HeadsetMonitor(applicationContext), BinauralAudioEngine())
|
||||
@@ -71,7 +90,11 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent { App(vm) }
|
||||
setContent {
|
||||
MaterialTheme(colorScheme = MindMachineColors) {
|
||||
App(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +116,7 @@ fun App(vm: MainViewModel = viewModel()) {
|
||||
}
|
||||
|
||||
val startRoute = if (ui.settings.safetyAcknowledged) "home" else "welcome"
|
||||
NavHost(navController = nav, startDestination = startRoute) {
|
||||
NavHost(navController = nav, startDestination = startRoute, modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||
composable("welcome") {
|
||||
SimpleScreen("MindMachine", "Blinking light + binaural audio. Stereo headphones required for binaural mode. Not a medical device.") {
|
||||
Button(onClick = { nav.navigate("safety") }) { Text("Continue") }
|
||||
@@ -118,10 +141,16 @@ fun App(vm: MainViewModel = viewModel()) {
|
||||
TextButton(onClick = { nav.navigate("holder") }) { Text("Holder Guidance") }
|
||||
}
|
||||
items(ui.presets) { p ->
|
||||
Card(Modifier.fillMaxWidth().clickable {
|
||||
vm.choosePreset(p.id)
|
||||
nav.navigate("setup")
|
||||
}.padding(4.dp)) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth().clickable {
|
||||
vm.choosePreset(p.id)
|
||||
nav.navigate("setup")
|
||||
}.padding(4.dp)
|
||||
) {
|
||||
Column(Modifier.padding(12.dp)) {
|
||||
Text(p.name, style = MaterialTheme.typography.titleMedium)
|
||||
Text(p.description)
|
||||
@@ -165,11 +194,11 @@ fun App(vm: MainViewModel = viewModel()) {
|
||||
@Composable
|
||||
fun SimpleScreen(title: String, subtitle: String, actions: @Composable ColumnScope.() -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(title, style = MaterialTheme.typography.headlineMedium)
|
||||
Text(subtitle)
|
||||
Text(title, style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text(subtitle, color = MaterialTheme.colorScheme.onBackground)
|
||||
actions()
|
||||
}
|
||||
}
|
||||
@@ -177,13 +206,16 @@ fun SimpleScreen(title: String, subtitle: String, actions: @Composable ColumnSco
|
||||
@Composable
|
||||
fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) {
|
||||
var checked by remember { mutableStateOf(false) }
|
||||
Column(Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Text("Read before using MindMachine", style = MaterialTheme.typography.headlineSmall)
|
||||
Column(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp)) {
|
||||
Text("Read before using MindMachine", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text("Flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers.\n\nDo not use while driving, walking, cycling, or operating machinery.\n\nStop immediately for discomfort, dizziness, headache, nausea, anxiety, or eye strain.\n\nBinaural mode requires stereo headphones.\n\nThis app is not a medical device.")
|
||||
Text(
|
||||
"Flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers.\n\nDo not use while driving, walking, cycling, or operating machinery.\n\nStop immediately for discomfort, dizziness, headache, nausea, anxiety, or eye strain.\n\nBinaural mode requires stereo headphones.\n\nThis app is not a medical device.",
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = checked, onCheckedChange = { checked = it })
|
||||
Text("I understand the risks and will stop immediately if I feel discomfort.")
|
||||
Text("I understand the risks and will stop immediately if I feel discomfort.", color = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
Button(onClick = onAck, enabled = checked, modifier = Modifier.semantics { contentDescription = "I Understand" }) { Text("I Understand") }
|
||||
TextButton(onClick = onHolder) { Text("Holder Guidance") }
|
||||
@@ -194,25 +226,28 @@ fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) {
|
||||
fun SetupScreen(vm: MainViewModel, onStart: () -> Unit, onHolder: () -> Unit) {
|
||||
val ui by vm.ui.collectAsStateWithLifecycle()
|
||||
val cfg = ui.config
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(ui.selectedPreset.name, style = MaterialTheme.typography.headlineSmall)
|
||||
Text("Duration: ${cfg.durationSec / 60} min")
|
||||
Column(
|
||||
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(ui.selectedPreset.name, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("Duration: ${cfg.durationSec / 60} min", color = MaterialTheme.colorScheme.onBackground)
|
||||
Slider(value = (cfg.durationSec / 60).toFloat(), onValueChange = { vm.setDurationMin(it.roundToInt()) }, valueRange = 1f..30f)
|
||||
Text("Mode")
|
||||
Text("Mode", color = MaterialTheme.colorScheme.onBackground)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SessionMode.values().forEach { mode ->
|
||||
OutlinedButton(onClick = { vm.setMode(mode) }) { Text(mode.name.replace("_", " ")) }
|
||||
}
|
||||
}
|
||||
Text("Blink ${cfg.blinkFrequencyHz} Hz")
|
||||
Text("Blink ${cfg.blinkFrequencyHz} Hz", color = MaterialTheme.colorScheme.onBackground)
|
||||
Slider(value = cfg.blinkFrequencyHz, onValueChange = vm::setBlinkFrequency, valueRange = 1f..20f)
|
||||
Text("Carrier ${cfg.carrierFrequencyHz.roundToInt()} Hz")
|
||||
Text("Carrier ${cfg.carrierFrequencyHz.roundToInt()} Hz", color = MaterialTheme.colorScheme.onBackground)
|
||||
Slider(value = cfg.carrierFrequencyHz, onValueChange = vm::setCarrier, valueRange = 80f..400f)
|
||||
Text("Difference ${cfg.binauralDifferenceHz} Hz")
|
||||
Text("Difference ${cfg.binauralDifferenceHz} Hz", color = MaterialTheme.colorScheme.onBackground)
|
||||
Slider(value = cfg.binauralDifferenceHz, onValueChange = vm::setDifference, valueRange = 0.5f..20f)
|
||||
Text("Brightness recommendation: keep screen comfortable and avoid eye strain.")
|
||||
Text("Brightness recommendation: keep screen comfortable and avoid eye strain.", color = MaterialTheme.colorScheme.onBackground)
|
||||
TextButton(onClick = onHolder) { Text("Holder Guidance") }
|
||||
if (ui.error != null) Text(ui.error!!, color = Color.Red)
|
||||
if (ui.error != null) Text(ui.error!!, color = MaterialTheme.colorScheme.error)
|
||||
Button(onClick = onStart, modifier = Modifier.fillMaxWidth().height(52.dp)) { Text("Start") }
|
||||
}
|
||||
}
|
||||
@@ -223,12 +258,19 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) {
|
||||
if (ui.runtimeState == RuntimeState.COMPLETED || ui.runtimeState == RuntimeState.STOPPED) onFinish()
|
||||
|
||||
var showOverlay by remember { mutableStateOf(true) }
|
||||
val intensity = (ui.config.intensityPercent / 100f)
|
||||
val flashing = if (ui.config.visualPatternType.name == "FLASH") {
|
||||
if ((ui.remainingSec % 2) == 0) intensity else 0f
|
||||
} else intensity * 0.5f
|
||||
val visualAlpha by rememberVisualAlpha(
|
||||
isRunning = ui.runtimeState == RuntimeState.RUNNING,
|
||||
mode = ui.config.mode,
|
||||
visualPattern = ui.config.visualPatternType,
|
||||
blinkFrequencyHz = ui.config.blinkFrequencyHz,
|
||||
intensityPercent = ui.config.intensityPercent
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().background(Color.White.copy(alpha = if (ui.config.mode == SessionMode.AUDIO_ONLY) 0f else flashing))
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.background(Color.White.copy(alpha = visualAlpha))
|
||||
.clickable { showOverlay = !showOverlay }
|
||||
) {
|
||||
if (ui.runtimeState == RuntimeState.COUNTDOWN) {
|
||||
@@ -236,11 +278,17 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) {
|
||||
"${ui.countdownSec}",
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
style = MaterialTheme.typography.displayLarge,
|
||||
color = Color.Black
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
if (showOverlay) {
|
||||
Column(Modifier.align(Alignment.BottomCenter).fillMaxWidth().background(Color.Black.copy(alpha = 0.6f)).padding(16.dp)) {
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(Color.Black.copy(alpha = 0.68f))
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("${ui.selectedPreset.name} • ${ui.remainingSec}s", color = Color.White)
|
||||
if (ui.runtimeState == RuntimeState.RUNNING) {
|
||||
Button(onClick = { vm.pause() }, modifier = Modifier.fillMaxWidth()) { Text("Pause") }
|
||||
@@ -261,14 +309,43 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberVisualAlpha(
|
||||
isRunning: Boolean,
|
||||
mode: SessionMode,
|
||||
visualPattern: VisualPattern,
|
||||
blinkFrequencyHz: Float,
|
||||
intensityPercent: Int,
|
||||
): androidx.compose.runtime.State<Float> {
|
||||
return produceState(initialValue = 0f, isRunning, mode, visualPattern, blinkFrequencyHz, intensityPercent) {
|
||||
if (!isRunning || mode == SessionMode.AUDIO_ONLY) {
|
||||
value = 0f
|
||||
return@produceState
|
||||
}
|
||||
while (true) {
|
||||
withFrameNanos { frameTimeNanos ->
|
||||
value = computeVisualAlpha(
|
||||
visualPattern = visualPattern,
|
||||
frameTimeNanos = frameTimeNanos,
|
||||
blinkFrequencyHz = blinkFrequencyHz,
|
||||
intensityPercent = intensityPercent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(vm: MainViewModel, onBack: () -> Unit) {
|
||||
val ui by vm.ui.collectAsStateWithLifecycle()
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Settings", style = MaterialTheme.typography.headlineSmall)
|
||||
Text("Countdown")
|
||||
Column(
|
||||
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text("Settings", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("Countdown", color = MaterialTheme.colorScheme.onBackground)
|
||||
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
|
||||
TextButton(onClick = { expanded = true }) { Text(ui.settings.countdownPreference.name) }
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
@@ -277,7 +354,7 @@ fun SettingsScreen(vm: MainViewModel, onBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("Default mode")
|
||||
Text("Default mode", color = MaterialTheme.colorScheme.onBackground)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SessionMode.values().forEach { mode ->
|
||||
OutlinedButton(onClick = { vm.updateDefaultMode(mode) }) { Text(mode.name) }
|
||||
@@ -285,24 +362,27 @@ fun SettingsScreen(vm: MainViewModel, onBack: () -> Unit) {
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = ui.settings.showHolderGuidanceBeforeSession, onCheckedChange = vm::updateGuidance)
|
||||
Text("Show holder guidance before session")
|
||||
Text("Show holder guidance before session", color = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Text("About/Disclaimer: MindMachine is a prototype and not a medical device.")
|
||||
Text("About/Disclaimer: MindMachine is a prototype and not a medical device.", color = MaterialTheme.colorScheme.onBackground)
|
||||
OutlinedButton(onClick = onBack) { Text("Back") }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HolderScreen(onBack: () -> Unit) {
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Holder Guidance", style = MaterialTheme.typography.headlineSmall)
|
||||
Text("• Use a simple cardboard visor/holder.")
|
||||
Text("• Keep phone stable and hands-free.")
|
||||
Text("• Do not press device against eyes/face.")
|
||||
Text("• Allow airflow and comfort.")
|
||||
Text("• Test fit before session.")
|
||||
Text("• Sit or lie down in a safe place.")
|
||||
Column(
|
||||
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text("Holder Guidance", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("• Use a simple cardboard visor/holder.", color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("• Keep phone stable and hands-free.", color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("• Do not press device against eyes/face.", color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("• Allow airflow and comfort.", color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("• Test fit before session.", color = MaterialTheme.colorScheme.onBackground)
|
||||
Text("• Sit or lie down in a safe place.", color = MaterialTheme.colorScheme.onBackground)
|
||||
OutlinedButton(onClick = onBack) { Text("Back") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.mindmachine.mvp.session
|
||||
|
||||
import com.mindmachine.mvp.domain.VisualPattern
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
|
||||
fun computeVisualAlpha(
|
||||
visualPattern: VisualPattern,
|
||||
frameTimeNanos: Long,
|
||||
blinkFrequencyHz: Float,
|
||||
intensityPercent: Int,
|
||||
): Float {
|
||||
val intensity = (intensityPercent.coerceIn(0, 100) / 100f)
|
||||
if (intensity <= 0f) return 0f
|
||||
|
||||
val frequency = blinkFrequencyHz.coerceIn(1f, 20f).toDouble()
|
||||
val timeSeconds = frameTimeNanos / 1_000_000_000.0
|
||||
|
||||
return when (visualPattern) {
|
||||
VisualPattern.FLASH -> {
|
||||
val phase = (timeSeconds * frequency) % 1.0
|
||||
if (phase < 0.5) intensity else 0f
|
||||
}
|
||||
|
||||
VisualPattern.PULSE -> {
|
||||
val pulse = ((sin(2.0 * PI * frequency * timeSeconds) + 1.0) / 2.0).toFloat()
|
||||
(intensity * pulse).coerceIn(0f, intensity)
|
||||
}
|
||||
}
|
||||
}
|
||||
67
app/src/test/java/com/mindmachine/mvp/VisualSignalTest.kt
Normal file
67
app/src/test/java/com/mindmachine/mvp/VisualSignalTest.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
package com.mindmachine.mvp
|
||||
|
||||
import com.mindmachine.mvp.domain.VisualPattern
|
||||
import com.mindmachine.mvp.session.computeVisualAlpha
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class VisualSignalTest {
|
||||
|
||||
@Test
|
||||
fun flashPatternAlternatesBetweenIntensityAndOff() {
|
||||
val intensity = 70
|
||||
val hz = 10f
|
||||
|
||||
val on = computeVisualAlpha(
|
||||
visualPattern = VisualPattern.FLASH,
|
||||
frameTimeNanos = 0L,
|
||||
blinkFrequencyHz = hz,
|
||||
intensityPercent = intensity
|
||||
)
|
||||
|
||||
val off = computeVisualAlpha(
|
||||
visualPattern = VisualPattern.FLASH,
|
||||
frameTimeNanos = 75_000_000L,
|
||||
blinkFrequencyHz = hz,
|
||||
intensityPercent = intensity
|
||||
)
|
||||
|
||||
assertEquals(0.7f, on, 0.0001f)
|
||||
assertEquals(0f, off, 0.0001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pulsePatternStaysWithinZeroAndIntensity() {
|
||||
val intensity = 60
|
||||
|
||||
val atStart = computeVisualAlpha(
|
||||
visualPattern = VisualPattern.PULSE,
|
||||
frameTimeNanos = 0L,
|
||||
blinkFrequencyHz = 3f,
|
||||
intensityPercent = intensity
|
||||
)
|
||||
val quarterCycle = computeVisualAlpha(
|
||||
visualPattern = VisualPattern.PULSE,
|
||||
frameTimeNanos = 83_333_333L,
|
||||
blinkFrequencyHz = 3f,
|
||||
intensityPercent = intensity
|
||||
)
|
||||
|
||||
assertTrue(atStart in 0f..0.6f)
|
||||
assertTrue(quarterCycle in 0f..0.6f)
|
||||
assertTrue(quarterCycle > atStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun intensityZeroAlwaysReturnsZero() {
|
||||
val alpha = computeVisualAlpha(
|
||||
visualPattern = VisualPattern.FLASH,
|
||||
frameTimeNanos = 123_000_000L,
|
||||
blinkFrequencyHz = 8f,
|
||||
intensityPercent = 0
|
||||
)
|
||||
|
||||
assertEquals(0f, alpha, 0f)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user