#ifndef TWISTY_TEXT_H__ #define TWISTY_TEXT_H__ #include #include "ILI9488_t3.h" #include "MathUtil.h" #include "TwistyTextFont.h" #include "BaseAnimation.h" const uint_fast8_t TEXT_PIXEL_WIDTH = 4; const float TEXT_PIXEL_HEIGHT = 12.0f; const float TEXT_3D_THICKNESS_MULT = 2.0f; const float LINE_SCROLL_SPEED = 0.004f; const float TWIST_AMOUNT = -0.0063f; const float WOBBLE_FREQ = 2.5f; const float WOBBLE_AMOUNT = 0.35f; const float WOBBLE_TORQUE = -0.19f; // 0.........10........20........30........40........ const char LINES[] = "HYDRONICS + ZKARCHER PRESENT: DEMOSAUCE!(){}(){}()"; const uint_fast8_t LINE_COUNT = 5; const uint_fast8_t CHARS_PER_LINE = 10; class TwistyText : public BaseAnimation { public: TwistyText() : BaseAnimation() {}; void init( ILI9488_t3 tft ); uint_fast16_t bgColor( void ); String title(); void reset( ILI9488_t3 tft ); boolean willForceTransition( void ); boolean forceTransitionNow( void ); void perFrame( ILI9488_t3 tft, FrameParams frameParams ); private: float _initPhase = 0; float _phase = 0; uint_fast16_t _bgColor; uint_fast8_t _meters[12]; boolean _drawnColumns[CHARS_PER_LINE*7]; }; void TwistyText::init( ILI9488_t3 tft ) { _bgColor = tft.color565( 0x0, 0x0, 0x33 ); } uint_fast16_t TwistyText::bgColor( void ) { return _bgColor; } String TwistyText::title() { return "TwistyText"; } void TwistyText::reset( ILI9488_t3 tft ) { _phase = _initPhase = LINE_COUNT * random(999); for( uint_fast8_t m=0; m<12; m++ ) { _meters[m] = 0; } for( uint_fast8_t c=0; c<(CHARS_PER_LINE*7); c++ ) { _drawnColumns[c] = false; } } boolean TwistyText::willForceTransition( void ) { return true; } boolean TwistyText::forceTransitionNow( void ) { return _phase > (_initPhase + LINE_COUNT); } void TwistyText::perFrame( ILI9488_t3 tft, FrameParams frameParams ) { uint_fast16_t w = (uint_fast16_t)tft.width(); uint_fast16_t h = (uint_fast16_t)tft.height(); uint_fast16_t w_2 = (w>>1); uint_fast16_t h_2 = (h>>1); uint_fast16_t paddingLeft = ((w - CHARS_PER_LINE * TEXT_PIXEL_WIDTH * 7) >> 1); paddingLeft += TEXT_PIXEL_WIDTH; // Because each character has 2 empty cols on the right _phase += frameParams.timeMult * LINE_SCROLL_SPEED; for( uint_fast8_t c=0; c<(CHARS_PER_LINE*7); c++ ) { uint_fast8_t columnInChar = c % 7; //if( columnInChar >= 5 ) continue; // Ignore spacing between characters uint_fast8_t charInLine = (c / 7); // Get the angle of rotation float angle = (_phase + c*TWIST_AMOUNT); // I want to pause so the user can read each line, so add some easing to hang out at // (angle%1.0)==0.5. float angleFloor = floor(angle); float decimal = angle - angleFloor; if( decimal < 0.5f ) { float inverse = (0.5f-decimal) * 2.0f; // 1..0 inverse *= (inverse*inverse); angle = angleFloor + (1.0f-inverse)*0.5f; } else { float ramp = (decimal*2.0f) - 1.0f; // 0..1 ramp *= (ramp*ramp); angle = angleFloor + 0.5f + ramp*0.5f; } // Hang on the final line, the hearts. Don't roll over to the first line. if( angle > _initPhase + LINE_COUNT - 0.5f ) { angle = _initPhase + LINE_COUNT - 0.5f; } angle *= M_PI; // Add wiggly wobble angle += sin( _phase*M_PI*WOBBLE_FREQ + (c*WOBBLE_TORQUE) ) * WOBBLE_AMOUNT; uint_fast16_t wrapAmt = floor( angle / M_PI ); angle -= wrapAmt * M_PI; // Get the text for this rotation uint_fast8_t lineIdx = wrapAmt % LINE_COUNT; char asciiValue = LINES[ lineIdx*CHARS_PER_LINE + charInLine ]; uint_fast8_t fontIdx; switch( asciiValue ) { case '+': fontIdx = 26; break; case ':': fontIdx = 27; break; case '-': fontIdx = 28; break; case '!': fontIdx = 29; break; case '(': fontIdx = 30; break; // heart (left) case ')': fontIdx = 31; break; // heart (right) case '{': fontIdx = 32; break; // skull (left) case '}': fontIdx = 33; break; // skull (right) default: fontIdx = asciiValue - 'A'; break; } uint_fast16_t charStart = fontIdx * 7; // 7 columns per character uint_fast8_t colByte = 0; if( (0 <= charStart) && (charStart < FONT_TABLE_LENGTH) ) { colByte = pgm_read_byte( &FONT_TABLE[ charStart + columnInChar ] ); } uint_fast16_t left = paddingLeft + c * TEXT_PIXEL_WIDTH; if( colByte == 0 ) { // Only erase columns if there's rects to erase from the last frame. if( _drawnColumns[c] ) { // *1.6: Cheap hack. Ensure rects from the previous frame are erased. tft.fillRect( left, h_2 - TEXT_PIXEL_HEIGHT*1.6, TEXT_PIXEL_WIDTH, TEXT_PIXEL_HEIGHT*TEXT_3D_THICKNESS_MULT*1.6, _bgColor ); _drawnColumns[c] = false; } continue; } _drawnColumns[c] = true; // Prepare to erase the background uint_fast16_t eraseTop = h_2 - 4.5*TEXT_PIXEL_HEIGHT; float cosAngle = cos( angle ); float cosAngleAbs = abs( cosAngle ); float sinAngle = sin( angle ); uint_fast16_t baseColor; switch( lineIdx ) { case 0: baseColor = 0x22ffff; break; // "HYDRONICS" case 1: baseColor = 0xffff44; break; // "+ ZKARCHER" case 2: baseColor = 0xff22ff; break; // "PRESENT" case 3: baseColor = 0x33ff88; break; // "SUPER-TFT!"; default: baseColor = 0xbb0000; break; // hearts } // Skull == grey if( (fontIdx==32) || (fontIdx==33) ) { baseColor = 0xaaaaaa; } // Material color: // cosAngleAbs: gives a color channel a more metallic appearance. // sinAngle: more traditional lighting. // tri: Happy medium. float tri = angle * (2.0f/M_PI); // 0..2 if( tri > 1.0 ) tri = 2.0f - tri; // 0..1..0 float triShiny = lerp( tri, tri*tri, 0.65f ); // Weight more towards grey. Material surface looks shiny. uint_fast16_t color = tft.color565( lerp8( 0x33, (baseColor&0xff0000)>>16, triShiny ), lerp8( 0x44, (baseColor&0x00ff00)>>8, triShiny ), lerp8( 0x44, (baseColor&0x0000ff), triShiny ) ); uint_fast16_t sideColor = tft.color565( 0xff * cosAngleAbs, 0x88 * cosAngleAbs, 0 ); uint_fast16_t sideHeight = abs( cosAngle ) * TEXT_PIXEL_HEIGHT * TEXT_3D_THICKNESS_MULT; // Draw a minimal number of rects. Advance from top to bottom. Track when rects start & end. boolean inRect = false; uint_fast8_t topBit; for( uint_fast8_t bit=0; bit<=8; bit++ ) { boolean isSolid = (boolean)( colByte & (0x1 << bit) ); if( !inRect && isSolid ) { inRect = true; topBit = bit; } else if( inRect && !isSolid ) { inRect = false; uint_fast16_t top = h_2 + ((int_fast8_t)(topBit-4) * TEXT_PIXEL_HEIGHT)*sinAngle - (TEXT_PIXEL_HEIGHT*cosAngle); uint_fast16_t height = ((bit-topBit) * TEXT_PIXEL_HEIGHT) * sinAngle; // Draw the pixel column tft.fillRect( left, top, TEXT_PIXEL_WIDTH, height, color ); // Draw the "side" hanging below, or above, depending on the twist angle if( cosAngle > 0.0f ) { // Side protudes below: (text is facing up) tft.fillRect( left, top+height, TEXT_PIXEL_WIDTH, sideHeight, sideColor ); // Erase above tft.fillRect( left, eraseTop, TEXT_PIXEL_WIDTH, top-eraseTop, _bgColor ); eraseTop = top + height + sideHeight; } else { // Side protrudes above: (text is facing down) uint_fast16_t drawTop = max( top-sideHeight, eraseTop ); // Don't draw over the letter material int_fast16_t drawHeight = top - drawTop; if( drawHeight > 0 ) { tft.fillRect( left, drawTop, TEXT_PIXEL_WIDTH, drawHeight, sideColor ); } // Erase above tft.fillRect( left, eraseTop, TEXT_PIXEL_WIDTH, (top-sideHeight)-eraseTop, _bgColor ); eraseTop = top + height; } } } // Erase old graphics below the column tft.fillRect( left, eraseTop, TEXT_PIXEL_WIDTH, (h_2+4.5*TEXT_PIXEL_HEIGHT)-eraseTop, _bgColor ); } // end each column // Super-awesome fake EQ meters const uint_fast8_t MAX_EQ_BARS = 24; const uint_fast8_t AUDIO_POWER = 96; // Seriously overdrive this for maximum meter awesomeness const uint_fast8_t EQ_BAR_WIDTH = 3; const uint_fast8_t EQ_BAR_HEIGHT = 5; const uint_fast8_t EQ_BAR_SPACING_X = 3; const uint_fast8_t EQ_BAR_SPACING_Y = 5; // Position EQ groups const uint_fast8_t EQ_GROUP_SEPARATE_X = 0; const uint_fast8_t EQ_GROUP_PADDING_Y = 13; uint_fast8_t baseEnergy = (uint_fast16_t)(frameParams.audioPeak * AUDIO_POWER) / 512; // Draw 4 groups: 2 on top, 2 on bottom, facing away from each other (out from center) uint_fast8_t meterOffset = 0; for( int_fast8_t eqX=-1; eqX<=1; eqX+=2 ) { uint_fast16_t baseX = w_2 + eqX*((EQ_GROUP_SEPARATE_X+EQ_BAR_WIDTH+EQ_BAR_SPACING_X)>>1); for( int_fast8_t eqY=-1; eqY<=1; eqY+=2 ) { // -1==bottom of screen, 1==top uint_fast16_t baseY = ( (eqY==1) ? EQ_GROUP_PADDING_Y : (h-EQ_GROUP_PADDING_Y-(EQ_BAR_HEIGHT)) ); int_fast8_t sideYOffset = (eqY==1) ? EQ_BAR_HEIGHT : -1; // pseudo-3D sides uint_fast8_t energy = baseEnergy; // Each meter steals some energy from audioEnergy uint_fast8_t newMeters[3]; newMeters[2] = random( (energy>>2), energy ); energy -= newMeters[2]; newMeters[1] = random( (energy>>1), energy ); energy -= newMeters[1]; newMeters[0] = energy; for( uint_fast8_t row=0; row<3; row++ ) { // 3 meter rows uint_fast16_t sideColor = tft.color565( 0xaa + row*0x20, 0x55 + row*0x10, 0 ); // Draw position uint_fast16_t drawY = baseY + row*eqY*(EQ_BAR_HEIGHT+EQ_BAR_SPACING_Y); uint_fast8_t prevMeter = _meters[meterOffset+row]; // Clamp the newMeters values to a sane range, please. // Avoid flicker at low volume levels. newMeters[row] = constrain( (uint_fast8_t)newMeters[row], (uint_fast8_t)1, (uint_fast8_t)MAX_EQ_BARS ); // Gently fade out over time newMeters[row] = max( _meters[meterOffset+row]>>1, newMeters[row] ); // Either draw or erase bricks, depending on whether the meter level increased or decreased boolean isErase = true; uint_fast16_t color = _bgColor; // default: erase uint_fast8_t drawHeight = EQ_BAR_HEIGHT; // If the energy of this row is higher: Set the draw color to something bright and colorful if( newMeters[row] > prevMeter ) { isErase = false; uint_fast8_t bright = 0x66 + row*0x33; color = tft.color565( 0x66, bright, bright ); } else { // Be sure to erase the pseudo-3D sides drawHeight++; if( eqY==-1 ) drawY--; } uint_fast8_t minMeter = min( newMeters[row], prevMeter ); uint_fast8_t maxMeter = max( newMeters[row], prevMeter ); for( uint_fast8_t m=minMeter; m