// ============================================================================
// MODE 16 - ANIMOWANA KASETA MAGNETOFONOWA
// ============================================================================
static void v50DrawCassetteReel(int cx, int cy, int tapeRadius, float angle)
{
gfx.tftFillCircle(cx, cy, tapeRadius + 3, 0x2104);
gfx.tftFillCircle(cx, cy, 25, 0xEF7D);
gfx.tftFillCircle(cx, cy, 15, 0xBDF7);
for (uint8_t tooth = 0; tooth < 8; ++tooth)
{
float a = angle + tooth * 0.7853982f;
int x1 = cx + lroundf(cosf(a) * 9.0f);
int y1 = cy + lroundf(sinf(a) * 9.0f);
int x2 = cx + lroundf(cosf(a) * 22.0f);
int y2 = cy + lroundf(sinf(a) * 22.0f);
gfx.tftDrawLine(x1, y1, x2, y2, 0x4208);
gfx.tftFillCircle(x2, y2, 2, 0x4208);
}
gfx.tftFillCircle(cx, cy, 6, 0x1082);
gfx.tftFillCircle(cx, cy, 3, 0xFFFF);
}
static void v50DrawCassetteLevel(int x, int y, int pct, const char *label)
{
gfx.tftText(x, y - 1, label, 1, 0xFFFF);
pct = constrain(pct, 0, 100);
for (uint8_t i = 0; i < 14; ++i)
{
int threshold = (i + 1) * 100 / 14;
uint16_t on = i < 9 ? 0x07E0 : (i < 12 ? 0xFFE0 : 0xF800);
gfx.tftFillRect(x + 12 + i * 9, y, 7, 7, threshold <= pct ? on : 0x2945);
}
}
// Czytelniejszy tekst na etykiecie kasety: pogrubienie 2 x 2 piksele.
static void v50DrawBoldText(int x, int y, const char *text, uint8_t size, uint16_t color)
{
if (text == nullptr || text[0] == '\0') return;
gfx.tftText(x, y, text, size, color);
gfx.tftText(x + 1, y, text, size, color);
gfx.tftText(x, y + 1, text, size, color);
gfx.tftText(x + 1, y + 1, text, size, color);
}
void v50PanelCassette()
{
static float reelAngle = 0.0f;
static float tapeTransfer = 0.0f;
static int8_t tapeDirection = 1;
static uint32_t lastCassetteMoveMs = 0;
static uint32_t playSeconds = 0;
static uint32_t lastSecondMs = 0;
static float meterL = 0.0f, meterR = 0.0f;
static float meterVelL = 0.0f, meterVelR = 0.0f;
static float previousAudioL = 0.0f, previousAudioR = 0.0f;
uint32_t now = millis();
bool playing = !volumeMute && SampleRate > 0;
if (playing && now - lastCassetteMoveMs >= 60UL)
{
lastCassetteMoveMs = now;
reelAngle -= 0.08f; // dodatni kąt = obrót przeciwnie z ruchem wskazówek
if (reelAngle < 0.0f) reelAngle += 6.2831853f;
tapeTransfer += tapeDirection * 0.00045f;
if (tapeTransfer >= 1.0f) { tapeTransfer = 1.0f; tapeDirection = -1; }
if (tapeTransfer <= 0.0f) { tapeTransfer = 0.0f; tapeDirection = 1; }
}
if (playing && now - lastSecondMs >= 1000UL)
{
lastSecondMs = now;
playSeconds = (playSeconds + 1) % 6000UL;
}
gfx.tftFillScreen(0x0000);
// Czerwono-czarna obudowa inspirowana klasyczną kasetą 90 min.
gfx.tftFillRoundRect(5, 5, 310, 230, 12, 0xA800);
gfx.tftDrawRoundRect(7, 7, 306, 226, 10, 0xFBAE);
gfx.tftFillRoundRect(12, 10, 296, 221, 8, 0x1082);
gfx.tftFillRect(16, 62, 288, 101, 0xB800);
// Biała etykieta: stała czcionka Unicode i przewijanie po pełnych znakach UTF-8.
// Font nie zmienia się w trakcie przewijania, a polskie litery są czytelne.
gfx.tftFillRoundRect(18, 10, 284, 50, 4, 0xFFFF);
gfx.tftDrawFastHLine(24, 34, 272, 0xE38E);
String radioLine = tftScrollWindowUtf8(tftStationName(), 34, 520);
String songLine = tftScrollWindowUtf8(tftNowPlaying(), 34, 360);
gfx.tftTextUtf8(25, 14, radioLine.c_str(), 1, 0x0000);
gfx.tftTextUtf8(25, 37, songLine.c_str(), 1, 0x0000);
// Przydymione okno kasety oraz zmieniająca się ilość taśmy na rolkach.
gfx.tftFillRoundRect(25, 70, 270, 85, 8, 0x0841);
gfx.tftDrawRoundRect(27, 72, 266, 81, 7, 0x7BEF);
int leftTapeRadius = 37 - lroundf(tapeTransfer * 13.0f);
int rightTapeRadius = 24 + lroundf(tapeTransfer * 13.0f);
v50DrawCassetteReel(92, 111, leftTapeRadius, reelAngle);
v50DrawCassetteReel(228, 111, rightTapeRadius, reelAngle);
gfx.tftDrawLine(92, 139, 228, 139, 0x8A22);
gfx.tftDrawLine(92, 140, 228, 140, 0xC326);
// Środkowa szybka z przesuwającą się taśmą.
gfx.tftFillRoundRect(126, 83, 68, 55, 4, 0x2104);
gfx.tftDrawRoundRect(128, 85, 64, 51, 3, 0x8410);
for (uint8_t i = 0; i < 7; ++i)
{
int px = 132 + ((i * 13 + (int)(reelAngle * 9.0f)) % 56);
gfx.tftFillRect(px, 108, 5, 3, 0xFDC8);
}
// Wyższy czerwony pasek i większa czcionka. Usunięcie podwójnych spacji
// pozwala zmieścić cały napis w szerokości 272 px przy rozmiarze 2.
gfx.tftFillRoundRect(24, 154, 272, 32, 3, 0xE920);
gfx.tftText(22, 160, "BASF90 EVO4 MULTI STEREO", 2, 0xFFFF);
float rawL = playing ? (float)tftVuPct(displayVuL) : 0.0f;
float rawR = playing ? (float)tftVuPct(displayVuR) : 0.0f;
float targetL = v50MusicBeatTarget(rawL, previousAudioL, 0.66f);
float targetR = v50MusicBeatTarget(rawR, previousAudioR, 0.66f);
meterVelL = (meterVelL + (targetL - meterL) * 0.64f) * 0.67f;
meterVelR = (meterVelR + (targetR - meterR) * 0.64f) * 0.67f;
meterL = constrain(meterL + meterVelL, 0.0f, 100.0f);
meterR = constrain(meterR + meterVelR, 0.0f, 100.0f);
int levelL = lroundf(meterL);
int levelR = lroundf(meterR);
v50DrawCassetteLevel(25, 190, levelL, "L");
v50DrawCassetteLevel(25, 203, levelR, "R");
char counter[8];
snprintf(counter, sizeof(counter), "%02lu:%02lu",
(unsigned long)(playSeconds / 60UL), (unsigned long)(playSeconds % 60UL));
gfx.tftText(250, 190, playing ? "PLAY" : "STOP", 1, playing ? 0x07FF : 0xF800);
gfx.tftText(250, 204, counter, 1, 0xFFE0);
gfx.tftText(278, 219, "M16", 1, 0xFFFF);
}