@@ -171,77 +171,59 @@ void DisplayManager::drawDashboard(const ScreenState& s) {
// =====================================================================
// Silence progress bar — with jitter/flash when charged
// =====================================================================
// =====================================================================
// Silence progress bar — gradient fill + breathing plateau on charge
// =====================================================================
void DisplayManager : : drawSilenceProgress ( float progress , bool charged ) {
int barX = 1 0;
int barY = SCREEN_HEIGHT - 4 0;
int barW = SCREEN_WIDTH - 2 0;
int barH = 30 ;
const int barX = 2 0;
const int barY = SCREEN_HEIGHT - 5 0; // near bottom of alert screen
const int barW = SCREEN_WIDTH - 4 0;
const int barH = 26 ;
const int radius = 6 ;
if ( charged ) {
// ---- CHARGED: jitter + flash effect ----
unsigned long elapsed = millis ( ) - _holdChargeMs ;
int cycle = ( elapsed / 60 ) % 6 ; // fast 60ms cycle, 6 frames
// ── Background track ──
_tft . fillRoundRect ( barX , barY , barW , barH , radius , COL_DARK_GRAY ) ;
// Jitter: offset bar position by ±2-3px
int jitterX = 0 ;
int jitterY = 0 ;
switch ( cycle ) {
case 0 : jitterX = - 3 ; jitterY = 1 ; break ;
case 1 : jitterX = 2 ; jitterY = - 2 ; break ;
case 2 : jitterX = - 1 ; jitterY = 2 ; break ;
case 3 : jitterX = 3 ; jitterY = - 1 ; break ;
case 4 : jitterX = - 2 ; jitterY = - 1 ; break ;
case 5 : jitterX = 1 ; jitterY = 2 ; break ;
if ( charged ) {
// ── Plateau: full bar with breathing pulse ──
// Sine-based brightness oscillation (~1 Hz)
float breath = ( sinf ( millis ( ) / 150.0f ) + 1.0f ) / 2.0f ; // 0.0– 1.0
uint8_t gLo = 42 , gHi = 63 ; // green channel range (RGB565 6-bit)
uint8_t g = gLo + ( uint8_t ) ( breath * ( float ) ( gHi - gLo ) ) ;
uint16_t pulseCol = ( g < < 5 ) ; // pure green in RGB565
_tft . fillRoundRect ( barX , barY , barW , barH , radius , pulseCol ) ;
_tft . drawRoundRect ( barX , barY , barW , barH , radius , COL_WHITE ) ;
_tft . setTextDatum ( MC_DATUM ) ;
_tft . setTextFont ( 2 ) ;
_tft . setTextSize ( 1 ) ;
_tft . setTextColor ( COL_WHITE , pulseCol ) ;
_tft . drawString ( " RELEASE " , barX + barW / 2 , barY + barH / 2 ) ;
return ;
}
// Clear slightly larger area to avoid jitter artifacts
_tft . fillRect ( barX - 4 , barY - 4 , barW + 8 , barH + 8 , COL_BLACK ) ;
// Flash between green and white
uint16_t flashCol = ( cycle % 2 = = 0 ) ? COL_GREEN : COL_WHITE ;
uint16_t textCol = ( cycle % 2 = = 0 ) ? COL_BLACK : COL_BLACK ;
_tft . fillRoundRect ( barX + jitterX , barY + jitterY , barW , barH , 6 , flashCol ) ;
_tft . drawRoundRect ( barX + jitterX , barY + jitterY , barW , barH , 6 , COL_YELLOW ) ;
// ">> RELEASE! <<" text
_tft . setTextFont ( 1 ) ;
_tft . setTextSize ( 2 ) ;
_tft . setTextDatum ( MC_DATUM ) ;
_tft . setTextColor ( textCol , flashCol ) ;
const char * labels [ ] = { " >> RELEASE! << " , " << RELEASE! >> " ,
" >> RELEASE! << " , " << RELEASE! >> " } ;
_tft . drawString ( labels [ cycle % 4 ] ,
SCREEN_WIDTH / 2 + jitterX ,
barY + barH / 2 + jitterY ) ;
} else {
// ---- FILLING: normal progress bar ----
int fillW = ( int ) ( barW * progress ) ;
_tft . fillRect ( barX , barY , barW , barH , COL_BLACK ) ;
if ( fillW > 0 ) {
// Gradient: dark green (left) → bright green (right)
for ( int i = 0 ; i < fillW ; i + + ) {
uint8_t g = map ( i , 0 , barW , 20 , 63 ) ; // 6-bit green channel (RGB565)
uint16_t col = ( g < < 5 ) ; // pure green in RGB565
_tft . drawFastVLine ( barX + i , barY , barH , col ) ;
}
// Redraw rounded border on top since the slices are square
_tft . drawRoundRect ( barX , barY , fillW , barH , 6 , COL_WHITE ) ;
// ── Filling: ease-out cubic ──
float eased = 1.0f - powf ( 1.0f - progress , 3.0f ) ;
int fillW = max ( 1 , ( int ) ( eased * ( float ) barW ) ) ;
// Gradient slices: dark green → bright green
uint8_t gMin = 16 , gMax = 58 ; // RGB565 green channel range
for ( int i = 0 ; i < fillW ; i + + ) {
float frac = ( float ) i / ( float ) barW ; // position relative to full bar
uint8_t g = gMin + ( uint8_t ) ( frac * ( float ) ( gMax - gMin ) ) ;
_tft . drawFastVLine ( barX + i , barY + 1 , barH - 2 , ( uint16_t ) ( g < < 5 ) ) ;
}
_tft . drawRoundRect ( barX , barY , barW , barH , 6 , COL_WHITE ) ;
// Percentage indicator inside bar
_tft . setTextFont ( 1 ) ;
_tft . setTextSize ( 2 ) ;
// Border on top of gradient slices
_tft . drawRoundRect ( barX , barY , barW , barH , radius , COL_WHITE ) ;
// Label
_tft . setTextDatum ( MC_DATUM ) ;
_tft . setTextColor ( COL_WHITE , COL_BLACK ) ;
_tft . drawString ( " HOLD TO SILENCE " ,
SCREEN_WIDTH / 2 , barY + barH / 2 ) ;
}
_tft . setTextFont ( 2 ) ;
_tft . setTextSize ( 1 ) ;
_tft . setTextColor ( COL_WHITE , COL_DARK_GRAY ) ;
_tft . drawString ( " HOLD " , barX + barW / 2 , barY + barH / 2 ) ;
}
// =====================================================================
@@ -386,3 +368,111 @@ void DisplayManager::drawStatusScreen(const ScreenState& s) {
drawCentered ( " tap to dismiss " , SCREEN_HEIGHT - 18 , 1 , COL_DARK_GRAY ) ;
}
// =====================================================================
// Hint animation — "nohup" coaching affordance
// =====================================================================
void DisplayManager : : startHintCycle ( ) {
_hint = HintAnim { } ; // reset everything
_hint . lastPlayMs = millis ( ) ; // will wait INITIAL_DELAY before first play
}
void DisplayManager : : stopHint ( ) {
_hint . running = false ;
}
bool DisplayManager : : updateHint ( ) {
unsigned long now = millis ( ) ;
// If a real hold is active, don't draw hints
// (caller should also just not call this, but belt-and-suspenders)
if ( _holdActive ) {
_hint . running = false ;
return false ;
}
// ── Not currently animating: check if it's time to start ──
if ( ! _hint . running ) {
unsigned long gap = _hint . lastPlayMs = = 0
? HintAnim : : INITIAL_DELAY
: HintAnim : : REPEAT_DELAY ;
if ( now - _hint . lastPlayMs > = gap ) {
_hint . running = true ;
_hint . startMs = now ;
} else {
return false ;
}
}
// ── Currently animating ──
unsigned long elapsed = now - _hint . startMs ;
if ( elapsed > _hint . totalDur ( ) ) {
// Animation complete — clear bar and schedule next
_hint . running = false ;
_hint . lastPlayMs = now ;
drawSilenceProgress ( 0.0f , false ) ; // clear to empty track
return true ;
}
float progress = 0.0f ;
if ( elapsed < HintAnim : : FILL_DUR ) {
// Phase 1: ease-in quadratic fill to peak
float t = ( float ) elapsed / ( float ) HintAnim : : FILL_DUR ;
progress = HintAnim : : PEAK * ( t * t ) ; // ease-in
} else if ( elapsed < HintAnim : : FILL_DUR + HintAnim : : HOLD_DUR ) {
// Phase 2: dwell at peak
progress = HintAnim : : PEAK ;
} else {
// Phase 3: ease-out quadratic drain
float t = ( float ) ( elapsed - HintAnim : : FILL_DUR - HintAnim : : HOLD_DUR )
/ ( float ) HintAnim : : DRAIN_DUR ;
progress = HintAnim : : PEAK * ( 1.0f - t * t ) ; // ease-out
}
drawHintBar ( progress ) ;
return true ;
}
void DisplayManager : : drawHintBar ( float progress ) {
const int barX = 20 ;
const int barY = SCREEN_HEIGHT - 50 ;
const int barW = SCREEN_WIDTH - 40 ;
const int barH = 26 ;
const int radius = 6 ;
// Background track
_tft . fillRoundRect ( barX , barY , barW , barH , radius , COL_DARK_GRAY ) ;
if ( progress > 0.001f ) {
int fillW = max ( 1 , ( int ) ( progress * ( float ) barW ) ) ;
// Ghost gradient: muted teal/cyan instead of green
// RGB565 pure-ish teal: low red, mid green, mid blue
for ( int i = 0 ; i < fillW ; i + + ) {
float frac = ( float ) i / ( float ) barW ;
uint8_t g = 12 + ( uint8_t ) ( frac * 18.0f ) ; // green 12– 30 (muted)
uint8_t b = 8 + ( uint8_t ) ( frac * 10.0f ) ; // blue 8– 18 (teal tint)
uint16_t col = ( g < < 5 ) | b ;
_tft . drawFastVLine ( barX + i , barY + 1 , barH - 2 , col ) ;
}
}
_tft . drawRoundRect ( barX , barY , barW , barH , radius , COL_GRAY ) ; // muted border (not white)
// Ghost label
_tft . setTextDatum ( MC_DATUM ) ;
_tft . setTextFont ( 2 ) ;
_tft . setTextSize ( 1 ) ;
_tft . setTextColor ( COL_GRAY , COL_DARK_GRAY ) ;
_tft . drawString ( " HOLD TO SILENCE " , barX + barW / 2 , barY + barH / 2 ) ;
}
float DisplayManager : : holdProgress ( ) const {
if ( ! _holdActive ) return 0.0f ;
return constrain ( ( float ) ( millis ( ) - _holdStartMs ) / ( float ) HOLD_DURATION_MS , 0.0f , 1.0f ) ;
}