Update mode toggle, split flash visuals, and interval-ms controls
This commit is contained in:
@@ -12,8 +12,7 @@
|
||||
android:theme="@style/Theme.MindMachine">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.mindmachine.mvp
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -29,14 +32,16 @@ import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
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.DisposableEffect
|
||||
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
|
||||
@@ -45,10 +50,10 @@ import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -63,9 +68,10 @@ 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.FlashColor
|
||||
import com.mindmachine.mvp.session.MainViewModel
|
||||
import com.mindmachine.mvp.session.computeVisualAlpha
|
||||
import com.mindmachine.mvp.session.SplitFlashFrame
|
||||
import com.mindmachine.mvp.session.computeSplitFlashFrame
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val MindMachineColors = darkColorScheme(
|
||||
@@ -105,7 +111,7 @@ fun App(vm: MainViewModel = viewModel()) {
|
||||
val ui by vm.ui.collectAsStateWithLifecycle()
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
|
||||
androidx.compose.runtime.DisposableEffect(lifecycle) {
|
||||
DisposableEffect(lifecycle) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_STOP && ui.runtimeState == RuntimeState.RUNNING) {
|
||||
vm.pause("Session paused because app moved to background.")
|
||||
@@ -234,13 +240,23 @@ fun SetupScreen(vm: MainViewModel, onStart: () -> Unit, onHolder: () -> Unit) {
|
||||
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", color = MaterialTheme.colorScheme.onBackground)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SessionMode.values().forEach { mode ->
|
||||
OutlinedButton(onClick = { vm.setMode(mode) }) { Text(mode.name.replace("_", " ")) }
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
SessionMode.values().forEachIndexed { index, mode ->
|
||||
SegmentedButton(
|
||||
shape = androidx.compose.material3.SegmentedButtonDefaults.itemShape(index = index, count = SessionMode.values().size),
|
||||
selected = cfg.mode == mode,
|
||||
onClick = { vm.setMode(mode) },
|
||||
label = { Text(mode.name.replace("_", " ")) }
|
||||
)
|
||||
}
|
||||
}
|
||||
Text("Blink ${cfg.blinkFrequencyHz} Hz", color = MaterialTheme.colorScheme.onBackground)
|
||||
Slider(value = cfg.blinkFrequencyHz, onValueChange = vm::setBlinkFrequency, valueRange = 1f..20f)
|
||||
Text("Flash interval: ${cfg.flashIntervalMs} ms", color = MaterialTheme.colorScheme.onBackground)
|
||||
Slider(
|
||||
value = cfg.flashIntervalMs.toFloat(),
|
||||
onValueChange = { vm.setFlashIntervalMs(it.roundToInt()) },
|
||||
valueRange = 50f..2000f
|
||||
)
|
||||
Text("Controls how long each frame is shown (red/green/black pattern).", color = MaterialTheme.colorScheme.onBackground)
|
||||
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", color = MaterialTheme.colorScheme.onBackground)
|
||||
@@ -257,22 +273,51 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) {
|
||||
val ui by vm.ui.collectAsStateWithLifecycle()
|
||||
if (ui.runtimeState == RuntimeState.COMPLETED || ui.runtimeState == RuntimeState.STOPPED) onFinish()
|
||||
|
||||
val isVisualMode = ui.config.mode != SessionMode.AUDIO_ONLY
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(isVisualMode) {
|
||||
val activity = context as? Activity
|
||||
val previous = activity?.requestedOrientation
|
||||
if (isVisualMode) {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
}
|
||||
onDispose {
|
||||
if (isVisualMode) {
|
||||
activity?.requestedOrientation = previous ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var showOverlay by remember { mutableStateOf(true) }
|
||||
val visualAlpha by rememberVisualAlpha(
|
||||
val flashFrame by rememberSplitFlashFrame(
|
||||
isRunning = ui.runtimeState == RuntimeState.RUNNING,
|
||||
mode = ui.config.mode,
|
||||
visualPattern = ui.config.visualPatternType,
|
||||
blinkFrequencyHz = ui.config.blinkFrequencyHz,
|
||||
intensityPercent = ui.config.intensityPercent
|
||||
flashIntervalMs = ui.config.flashIntervalMs,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.background(Color.White.copy(alpha = visualAlpha))
|
||||
.clickable { showOverlay = !showOverlay }
|
||||
) {
|
||||
if (isVisualMode) {
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.background(flashFrame.left.toComposeColor())
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.background(flashFrame.right.toComposeColor())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (ui.runtimeState == RuntimeState.COUNTDOWN) {
|
||||
Text(
|
||||
"${ui.countdownSec}",
|
||||
@@ -286,7 +331,7 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) {
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(Color.Black.copy(alpha = 0.68f))
|
||||
.background(Color.Black.copy(alpha = 0.72f))
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("${ui.selectedPreset.name} • ${ui.remainingSec}s", color = Color.White)
|
||||
@@ -309,28 +354,25 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun FlashColor.toComposeColor(): Color = when (this) {
|
||||
FlashColor.RED -> Color.Red
|
||||
FlashColor.GREEN -> Color.Green
|
||||
FlashColor.BLACK -> Color.Black
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberVisualAlpha(
|
||||
private fun rememberSplitFlashFrame(
|
||||
isRunning: Boolean,
|
||||
mode: SessionMode,
|
||||
visualPattern: VisualPattern,
|
||||
blinkFrequencyHz: Float,
|
||||
intensityPercent: Int,
|
||||
): androidx.compose.runtime.State<Float> {
|
||||
return produceState(initialValue = 0f, isRunning, mode, visualPattern, blinkFrequencyHz, intensityPercent) {
|
||||
flashIntervalMs: Int,
|
||||
) = produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning, mode, flashIntervalMs) {
|
||||
if (!isRunning || mode == SessionMode.AUDIO_ONLY) {
|
||||
value = 0f
|
||||
value = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK)
|
||||
return@produceState
|
||||
}
|
||||
while (true) {
|
||||
withFrameNanos { frameTimeNanos ->
|
||||
value = computeVisualAlpha(
|
||||
visualPattern = visualPattern,
|
||||
frameTimeNanos = frameTimeNanos,
|
||||
blinkFrequencyHz = blinkFrequencyHz,
|
||||
intensityPercent = intensityPercent
|
||||
)
|
||||
}
|
||||
value = computeSplitFlashFrame(frameTimeNanos, flashIntervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ data class SessionPreset(
|
||||
val description: String,
|
||||
val defaultDurationSec: Int,
|
||||
val visualPatternType: VisualPattern,
|
||||
val blinkFrequencyHz: Float,
|
||||
val flashIntervalMs: Int,
|
||||
val intensityPercent: Int,
|
||||
val carrierFrequencyHz: Float,
|
||||
val binauralDifferenceHz: Float,
|
||||
@@ -24,7 +24,7 @@ data class SessionConfig(
|
||||
val durationSec: Int,
|
||||
val mode: SessionMode,
|
||||
val visualPatternType: VisualPattern,
|
||||
val blinkFrequencyHz: Float,
|
||||
val flashIntervalMs: Int,
|
||||
val intensityPercent: Int,
|
||||
val carrierFrequencyHz: Float,
|
||||
val binauralDifferenceHz: Float,
|
||||
@@ -43,17 +43,17 @@ object Presets {
|
||||
val builtIn = listOf(
|
||||
SessionPreset(
|
||||
"relax", "Relax", "Gentle pulse, slower beat", 10 * 60,
|
||||
VisualPattern.PULSE, 6f, 60, 200f, 6f,
|
||||
VisualPattern.PULSE, 167, 60, 200f, 6f,
|
||||
"Stop if discomfort occurs.", 1
|
||||
),
|
||||
SessionPreset(
|
||||
"focus", "Focus", "Steady flash, conservative alert beat", 15 * 60,
|
||||
VisualPattern.FLASH, 10f, 65, 220f, 10f,
|
||||
VisualPattern.FLASH, 100, 65, 220f, 10f,
|
||||
"Use in a safe seated place only.", 2
|
||||
),
|
||||
SessionPreset(
|
||||
"sleep", "Sleep Prep", "Slow pulse, low intensity", 20 * 60,
|
||||
VisualPattern.PULSE, 3f, 45, 180f, 3f,
|
||||
VisualPattern.PULSE, 333, 45, 180f, 3f,
|
||||
"Do not use while doing other activities.", 3
|
||||
)
|
||||
)
|
||||
@@ -64,7 +64,7 @@ fun SessionPreset.toConfig(defaultMode: SessionMode = SessionMode.AUDIO_VISUAL)
|
||||
durationSec = defaultDurationSec,
|
||||
mode = defaultMode,
|
||||
visualPatternType = visualPatternType,
|
||||
blinkFrequencyHz = blinkFrequencyHz,
|
||||
flashIntervalMs = flashIntervalMs,
|
||||
intensityPercent = intensityPercent,
|
||||
carrierFrequencyHz = carrierFrequencyHz,
|
||||
binauralDifferenceHz = binauralDifferenceHz,
|
||||
|
||||
@@ -67,7 +67,7 @@ class MainViewModel(
|
||||
|
||||
fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }
|
||||
fun setDurationMin(min: Int) = _ui.update { it.copy(config = it.config.copy(durationSec = (min.coerceIn(1, 30) * 60)), error = null) }
|
||||
fun setBlinkFrequency(value: Float) = _ui.update { it.copy(config = it.config.copy(blinkFrequencyHz = value.coerceIn(1f, 20f)), error = null) }
|
||||
fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) }
|
||||
fun setCarrier(value: Float) = _ui.update { it.copy(config = it.config.copy(carrierFrequencyHz = value.coerceIn(80f, 400f)), error = null) }
|
||||
fun setDifference(value: Float) = _ui.update { it.copy(config = it.config.copy(binauralDifferenceHz = value.coerceIn(0.5f, 20f)), error = null) }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.mindmachine.mvp.domain.SessionMode
|
||||
object SessionValidator {
|
||||
fun validate(config: SessionConfig, headsetAvailable: Boolean): String? {
|
||||
if (config.durationSec !in 60..(30 * 60)) return "Duration must be 1 to 30 minutes."
|
||||
if (config.blinkFrequencyHz !in 1f..20f) return "Blink frequency must be 1.0 to 20.0 Hz."
|
||||
if (config.flashIntervalMs !in 50..2000) return "Flash interval must be 50 to 2000 ms."
|
||||
if (config.carrierFrequencyHz !in 80f..400f) return "Carrier frequency must be 80 to 400 Hz."
|
||||
if (config.binauralDifferenceHz !in 0.5f..20f) return "Binaural difference must be 0.5 to 20 Hz."
|
||||
if (config.mode != SessionMode.VISUAL_ONLY && !headsetAvailable) {
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
package com.mindmachine.mvp.session
|
||||
|
||||
import com.mindmachine.mvp.domain.VisualPattern
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
enum class FlashColor {
|
||||
RED,
|
||||
GREEN,
|
||||
BLACK,
|
||||
}
|
||||
|
||||
fun computeVisualAlpha(
|
||||
visualPattern: VisualPattern,
|
||||
data class SplitFlashFrame(
|
||||
val left: FlashColor,
|
||||
val right: FlashColor,
|
||||
)
|
||||
|
||||
fun computeSplitFlashFrame(
|
||||
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)
|
||||
}
|
||||
flashIntervalMs: Int,
|
||||
): SplitFlashFrame {
|
||||
val intervalMs = flashIntervalMs.coerceIn(50, 2000).toLong()
|
||||
val elapsedMs = frameTimeNanos / 1_000_000L
|
||||
return when (((elapsedMs / intervalMs) % 4).toInt()) {
|
||||
0 -> SplitFlashFrame(left = FlashColor.RED, right = FlashColor.GREEN)
|
||||
1 -> SplitFlashFrame(left = FlashColor.BLACK, right = FlashColor.BLACK)
|
||||
2 -> SplitFlashFrame(left = FlashColor.GREEN, right = FlashColor.RED)
|
||||
else -> SplitFlashFrame(left = FlashColor.BLACK, right = FlashColor.BLACK)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ class PresetDefaultsTest {
|
||||
|
||||
assertEquals(10 * 60, relax.defaultDurationSec)
|
||||
assertEquals(VisualPattern.PULSE, relax.visualPatternType)
|
||||
assertEquals(6f, relax.blinkFrequencyHz)
|
||||
assertEquals(167, relax.flashIntervalMs)
|
||||
|
||||
assertEquals(15 * 60, focus.defaultDurationSec)
|
||||
assertEquals(VisualPattern.FLASH, focus.visualPatternType)
|
||||
assertEquals(10f, focus.blinkFrequencyHz)
|
||||
assertEquals(100, focus.flashIntervalMs)
|
||||
|
||||
assertEquals(20 * 60, sleep.defaultDurationSec)
|
||||
assertEquals(VisualPattern.PULSE, sleep.visualPatternType)
|
||||
assertEquals(3f, sleep.blinkFrequencyHz)
|
||||
assertEquals(333, sleep.flashIntervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.mindmachine.mvp
|
||||
|
||||
import com.mindmachine.mvp.domain.SessionConfig
|
||||
import com.mindmachine.mvp.domain.SessionMode
|
||||
import com.mindmachine.mvp.domain.VisualPattern
|
||||
import com.mindmachine.mvp.domain.SessionConfig
|
||||
import com.mindmachine.mvp.session.SessionValidator
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
@@ -14,7 +14,7 @@ class SessionValidatorTest {
|
||||
durationSec = 600,
|
||||
mode = SessionMode.AUDIO_VISUAL,
|
||||
visualPatternType = VisualPattern.PULSE,
|
||||
blinkFrequencyHz = 6f,
|
||||
flashIntervalMs = 167,
|
||||
intensityPercent = 50,
|
||||
carrierFrequencyHz = 200f,
|
||||
binauralDifferenceHz = 6f,
|
||||
@@ -39,7 +39,7 @@ class SessionValidatorTest {
|
||||
@Test
|
||||
fun out_of_range_values_are_blocked() {
|
||||
assertEquals("Duration must be 1 to 30 minutes.", SessionValidator.validate(valid.copy(durationSec = 59), true))
|
||||
assertEquals("Blink frequency must be 1.0 to 20.0 Hz.", SessionValidator.validate(valid.copy(blinkFrequencyHz = 21f), true))
|
||||
assertEquals("Flash interval must be 50 to 2000 ms.", SessionValidator.validate(valid.copy(flashIntervalMs = 49), true))
|
||||
assertEquals("Carrier frequency must be 80 to 400 Hz.", SessionValidator.validate(valid.copy(carrierFrequencyHz = 79f), true))
|
||||
assertEquals("Binaural difference must be 0.5 to 20 Hz.", SessionValidator.validate(valid.copy(binauralDifferenceHz = 0.1f), true))
|
||||
}
|
||||
|
||||
@@ -1,67 +1,42 @@
|
||||
package com.mindmachine.mvp
|
||||
|
||||
import com.mindmachine.mvp.domain.VisualPattern
|
||||
import com.mindmachine.mvp.session.computeVisualAlpha
|
||||
import com.mindmachine.mvp.session.FlashColor
|
||||
import com.mindmachine.mvp.session.computeSplitFlashFrame
|
||||
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
|
||||
fun splitFlashPattern_cycles_redGreen_black_greenRed_black() {
|
||||
val intervalMs = 100
|
||||
|
||||
val on = computeVisualAlpha(
|
||||
visualPattern = VisualPattern.FLASH,
|
||||
frameTimeNanos = 0L,
|
||||
blinkFrequencyHz = hz,
|
||||
intensityPercent = intensity
|
||||
)
|
||||
val phase0 = computeSplitFlashFrame(frameTimeNanos = 0L, flashIntervalMs = intervalMs)
|
||||
val phase1 = computeSplitFlashFrame(frameTimeNanos = 100_000_000L, flashIntervalMs = intervalMs)
|
||||
val phase2 = computeSplitFlashFrame(frameTimeNanos = 200_000_000L, flashIntervalMs = intervalMs)
|
||||
val phase3 = computeSplitFlashFrame(frameTimeNanos = 300_000_000L, flashIntervalMs = intervalMs)
|
||||
|
||||
val off = computeVisualAlpha(
|
||||
visualPattern = VisualPattern.FLASH,
|
||||
frameTimeNanos = 75_000_000L,
|
||||
blinkFrequencyHz = hz,
|
||||
intensityPercent = intensity
|
||||
)
|
||||
assertEquals(FlashColor.RED, phase0.left)
|
||||
assertEquals(FlashColor.GREEN, phase0.right)
|
||||
|
||||
assertEquals(0.7f, on, 0.0001f)
|
||||
assertEquals(0f, off, 0.0001f)
|
||||
assertEquals(FlashColor.BLACK, phase1.left)
|
||||
assertEquals(FlashColor.BLACK, phase1.right)
|
||||
|
||||
assertEquals(FlashColor.GREEN, phase2.left)
|
||||
assertEquals(FlashColor.RED, phase2.right)
|
||||
|
||||
assertEquals(FlashColor.BLACK, phase3.left)
|
||||
assertEquals(FlashColor.BLACK, phase3.right)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pulsePatternStaysWithinZeroAndIntensity() {
|
||||
val intensity = 60
|
||||
fun interval_is_clamped_to_valid_range() {
|
||||
val tooFast = computeSplitFlashFrame(frameTimeNanos = 49_000_000L, flashIntervalMs = 1)
|
||||
val validAt50ms = computeSplitFlashFrame(frameTimeNanos = 49_000_000L, flashIntervalMs = 50)
|
||||
assertEquals(validAt50ms, tooFast)
|
||||
|
||||
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)
|
||||
val tooSlow = computeSplitFlashFrame(frameTimeNanos = 2_100_000_000L, flashIntervalMs = 5000)
|
||||
val validAt2000ms = computeSplitFlashFrame(frameTimeNanos = 2_100_000_000L, flashIntervalMs = 2000)
|
||||
assertEquals(validAt2000ms, tooSlow)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user