PlatformIO package of the Teensy core framework compatible with GCC 10 & C++20
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 line
11KB

  1. #ifndef TWISTY_TEXT_H__
  2. #define TWISTY_TEXT_H__
  3. #include <Arduino.h>
  4. #include "ILI9488_t3.h"
  5. #include "MathUtil.h"
  6. #include "TwistyTextFont.h"
  7. #include "BaseAnimation.h"
  8. const uint_fast8_t TEXT_PIXEL_WIDTH = 4;
  9. const float TEXT_PIXEL_HEIGHT = 12.0f;
  10. const float TEXT_3D_THICKNESS_MULT = 2.0f;
  11. const float LINE_SCROLL_SPEED = 0.004f;
  12. const float TWIST_AMOUNT = -0.0063f;
  13. const float WOBBLE_FREQ = 2.5f;
  14. const float WOBBLE_AMOUNT = 0.35f;
  15. const float WOBBLE_TORQUE = -0.19f;
  16. // 0.........10........20........30........40........
  17. const char LINES[] = "HYDRONICS + ZKARCHER PRESENT: DEMOSAUCE!(){}(){}()";
  18. const uint_fast8_t LINE_COUNT = 5;
  19. const uint_fast8_t CHARS_PER_LINE = 10;
  20. class TwistyText : public BaseAnimation {
  21. public:
  22. TwistyText() : BaseAnimation() {};
  23. void init( ILI9488_t3 tft );
  24. uint_fast16_t bgColor( void );
  25. String title();
  26. void reset( ILI9488_t3 tft );
  27. boolean willForceTransition( void );
  28. boolean forceTransitionNow( void );
  29. void perFrame( ILI9488_t3 tft, FrameParams frameParams );
  30. private:
  31. float _initPhase = 0;
  32. float _phase = 0;
  33. uint_fast16_t _bgColor;
  34. uint_fast8_t _meters[12];
  35. boolean _drawnColumns[CHARS_PER_LINE*7];
  36. };
  37. void TwistyText::init( ILI9488_t3 tft ) {
  38. _bgColor = tft.color565( 0x0, 0x0, 0x33 );
  39. }
  40. uint_fast16_t TwistyText::bgColor( void ) {
  41. return _bgColor;
  42. }
  43. String TwistyText::title() {
  44. return "TwistyText";
  45. }
  46. void TwistyText::reset( ILI9488_t3 tft ) {
  47. _phase = _initPhase = LINE_COUNT * random(999);
  48. for( uint_fast8_t m=0; m<12; m++ ) {
  49. _meters[m] = 0;
  50. }
  51. for( uint_fast8_t c=0; c<(CHARS_PER_LINE*7); c++ ) {
  52. _drawnColumns[c] = false;
  53. }
  54. }
  55. boolean TwistyText::willForceTransition( void ) {
  56. return true;
  57. }
  58. boolean TwistyText::forceTransitionNow( void ) {
  59. return _phase > (_initPhase + LINE_COUNT);
  60. }
  61. void TwistyText::perFrame( ILI9488_t3 tft, FrameParams frameParams ) {
  62. uint_fast16_t w = (uint_fast16_t)tft.width();
  63. uint_fast16_t h = (uint_fast16_t)tft.height();
  64. uint_fast16_t w_2 = (w>>1);
  65. uint_fast16_t h_2 = (h>>1);
  66. uint_fast16_t paddingLeft = ((w - CHARS_PER_LINE * TEXT_PIXEL_WIDTH * 7) >> 1);
  67. paddingLeft += TEXT_PIXEL_WIDTH; // Because each character has 2 empty cols on the right
  68. _phase += frameParams.timeMult * LINE_SCROLL_SPEED;
  69. for( uint_fast8_t c=0; c<(CHARS_PER_LINE*7); c++ ) {
  70. uint_fast8_t columnInChar = c % 7;
  71. //if( columnInChar >= 5 ) continue; // Ignore spacing between characters
  72. uint_fast8_t charInLine = (c / 7);
  73. // Get the angle of rotation
  74. float angle = (_phase + c*TWIST_AMOUNT);
  75. // I want to pause so the user can read each line, so add some easing to hang out at
  76. // (angle%1.0)==0.5.
  77. float angleFloor = floor(angle);
  78. float decimal = angle - angleFloor;
  79. if( decimal < 0.5f ) {
  80. float inverse = (0.5f-decimal) * 2.0f; // 1..0
  81. inverse *= (inverse*inverse);
  82. angle = angleFloor + (1.0f-inverse)*0.5f;
  83. } else {
  84. float ramp = (decimal*2.0f) - 1.0f; // 0..1
  85. ramp *= (ramp*ramp);
  86. angle = angleFloor + 0.5f + ramp*0.5f;
  87. }
  88. // Hang on the final line, the hearts. Don't roll over to the first line.
  89. if( angle > _initPhase + LINE_COUNT - 0.5f ) {
  90. angle = _initPhase + LINE_COUNT - 0.5f;
  91. }
  92. angle *= M_PI;
  93. // Add wiggly wobble
  94. angle += sin( _phase*M_PI*WOBBLE_FREQ + (c*WOBBLE_TORQUE) ) * WOBBLE_AMOUNT;
  95. uint_fast16_t wrapAmt = floor( angle / M_PI );
  96. angle -= wrapAmt * M_PI;
  97. // Get the text for this rotation
  98. uint_fast8_t lineIdx = wrapAmt % LINE_COUNT;
  99. char asciiValue = LINES[ lineIdx*CHARS_PER_LINE + charInLine ];
  100. uint_fast8_t fontIdx;
  101. switch( asciiValue ) {
  102. case '+': fontIdx = 26; break;
  103. case ':': fontIdx = 27; break;
  104. case '-': fontIdx = 28; break;
  105. case '!': fontIdx = 29; break;
  106. case '(': fontIdx = 30; break; // heart (left)
  107. case ')': fontIdx = 31; break; // heart (right)
  108. case '{': fontIdx = 32; break; // skull (left)
  109. case '}': fontIdx = 33; break; // skull (right)
  110. default: fontIdx = asciiValue - 'A'; break;
  111. }
  112. uint_fast16_t charStart = fontIdx * 7; // 7 columns per character
  113. uint_fast8_t colByte = 0;
  114. if( (0 <= charStart) && (charStart < FONT_TABLE_LENGTH) ) {
  115. colByte = pgm_read_byte( &FONT_TABLE[ charStart + columnInChar ] );
  116. }
  117. uint_fast16_t left = paddingLeft + c * TEXT_PIXEL_WIDTH;
  118. if( colByte == 0 ) {
  119. // Only erase columns if there's rects to erase from the last frame.
  120. if( _drawnColumns[c] ) {
  121. // *1.6: Cheap hack. Ensure rects from the previous frame are erased.
  122. tft.fillRect( left, h_2 - TEXT_PIXEL_HEIGHT*1.6, TEXT_PIXEL_WIDTH, TEXT_PIXEL_HEIGHT*TEXT_3D_THICKNESS_MULT*1.6, _bgColor );
  123. _drawnColumns[c] = false;
  124. }
  125. continue;
  126. }
  127. _drawnColumns[c] = true;
  128. // Prepare to erase the background
  129. uint_fast16_t eraseTop = h_2 - 4.5*TEXT_PIXEL_HEIGHT;
  130. float cosAngle = cos( angle );
  131. float cosAngleAbs = abs( cosAngle );
  132. float sinAngle = sin( angle );
  133. uint_fast16_t baseColor;
  134. switch( lineIdx ) {
  135. case 0: baseColor = 0x22ffff; break; // "HYDRONICS"
  136. case 1: baseColor = 0xffff44; break; // "+ ZKARCHER"
  137. case 2: baseColor = 0xff22ff; break; // "PRESENT"
  138. case 3: baseColor = 0x33ff88; break; // "SUPER-TFT!";
  139. default: baseColor = 0xbb0000; break; // hearts
  140. }
  141. // Skull == grey
  142. if( (fontIdx==32) || (fontIdx==33) ) {
  143. baseColor = 0xaaaaaa;
  144. }
  145. // Material color:
  146. // cosAngleAbs: gives a color channel a more metallic appearance.
  147. // sinAngle: more traditional lighting.
  148. // tri: Happy medium.
  149. float tri = angle * (2.0f/M_PI); // 0..2
  150. if( tri > 1.0 ) tri = 2.0f - tri; // 0..1..0
  151. float triShiny = lerp( tri, tri*tri, 0.65f ); // Weight more towards grey. Material surface looks shiny.
  152. uint_fast16_t color = tft.color565(
  153. lerp8( 0x33, (baseColor&0xff0000)>>16, triShiny ),
  154. lerp8( 0x44, (baseColor&0x00ff00)>>8, triShiny ),
  155. lerp8( 0x44, (baseColor&0x0000ff), triShiny )
  156. );
  157. uint_fast16_t sideColor = tft.color565( 0xff * cosAngleAbs, 0x88 * cosAngleAbs, 0 );
  158. uint_fast16_t sideHeight = abs( cosAngle ) * TEXT_PIXEL_HEIGHT * TEXT_3D_THICKNESS_MULT;
  159. // Draw a minimal number of rects. Advance from top to bottom. Track when rects start & end.
  160. boolean inRect = false;
  161. uint_fast8_t topBit;
  162. for( uint_fast8_t bit=0; bit<=8; bit++ ) {
  163. boolean isSolid = (boolean)( colByte & (0x1 << bit) );
  164. if( !inRect && isSolid ) {
  165. inRect = true;
  166. topBit = bit;
  167. } else if( inRect && !isSolid ) {
  168. inRect = false;
  169. uint_fast16_t top = h_2 + ((int_fast8_t)(topBit-4) * TEXT_PIXEL_HEIGHT)*sinAngle - (TEXT_PIXEL_HEIGHT*cosAngle);
  170. uint_fast16_t height = ((bit-topBit) * TEXT_PIXEL_HEIGHT) * sinAngle;
  171. // Draw the pixel column
  172. tft.fillRect( left, top, TEXT_PIXEL_WIDTH, height, color );
  173. // Draw the "side" hanging below, or above, depending on the twist angle
  174. if( cosAngle > 0.0f ) {
  175. // Side protudes below: (text is facing up)
  176. tft.fillRect( left, top+height, TEXT_PIXEL_WIDTH, sideHeight, sideColor );
  177. // Erase above
  178. tft.fillRect( left, eraseTop, TEXT_PIXEL_WIDTH, top-eraseTop, _bgColor );
  179. eraseTop = top + height + sideHeight;
  180. } else {
  181. // Side protrudes above: (text is facing down)
  182. uint_fast16_t drawTop = max( top-sideHeight, eraseTop ); // Don't draw over the letter material
  183. int_fast16_t drawHeight = top - drawTop;
  184. if( drawHeight > 0 ) {
  185. tft.fillRect( left, drawTop, TEXT_PIXEL_WIDTH, drawHeight, sideColor );
  186. }
  187. // Erase above
  188. tft.fillRect( left, eraseTop, TEXT_PIXEL_WIDTH, (top-sideHeight)-eraseTop, _bgColor );
  189. eraseTop = top + height;
  190. }
  191. }
  192. }
  193. // Erase old graphics below the column
  194. tft.fillRect( left, eraseTop, TEXT_PIXEL_WIDTH, (h_2+4.5*TEXT_PIXEL_HEIGHT)-eraseTop, _bgColor );
  195. } // end each column
  196. // Super-awesome fake EQ meters
  197. const uint_fast8_t MAX_EQ_BARS = 24;
  198. const uint_fast8_t AUDIO_POWER = 96; // Seriously overdrive this for maximum meter awesomeness
  199. const uint_fast8_t EQ_BAR_WIDTH = 3;
  200. const uint_fast8_t EQ_BAR_HEIGHT = 5;
  201. const uint_fast8_t EQ_BAR_SPACING_X = 3;
  202. const uint_fast8_t EQ_BAR_SPACING_Y = 5;
  203. // Position EQ groups
  204. const uint_fast8_t EQ_GROUP_SEPARATE_X = 0;
  205. const uint_fast8_t EQ_GROUP_PADDING_Y = 13;
  206. uint_fast8_t baseEnergy = (uint_fast16_t)(frameParams.audioPeak * AUDIO_POWER) / 512;
  207. // Draw 4 groups: 2 on top, 2 on bottom, facing away from each other (out from center)
  208. uint_fast8_t meterOffset = 0;
  209. for( int_fast8_t eqX=-1; eqX<=1; eqX+=2 ) {
  210. uint_fast16_t baseX = w_2 + eqX*((EQ_GROUP_SEPARATE_X+EQ_BAR_WIDTH+EQ_BAR_SPACING_X)>>1);
  211. for( int_fast8_t eqY=-1; eqY<=1; eqY+=2 ) { // -1==bottom of screen, 1==top
  212. uint_fast16_t baseY = ( (eqY==1) ? EQ_GROUP_PADDING_Y : (h-EQ_GROUP_PADDING_Y-(EQ_BAR_HEIGHT)) );
  213. int_fast8_t sideYOffset = (eqY==1) ? EQ_BAR_HEIGHT : -1; // pseudo-3D sides
  214. uint_fast8_t energy = baseEnergy;
  215. // Each meter steals some energy from audioEnergy
  216. uint_fast8_t newMeters[3];
  217. newMeters[2] = random( (energy>>2), energy );
  218. energy -= newMeters[2];
  219. newMeters[1] = random( (energy>>1), energy );
  220. energy -= newMeters[1];
  221. newMeters[0] = energy;
  222. for( uint_fast8_t row=0; row<3; row++ ) { // 3 meter rows
  223. uint_fast16_t sideColor = tft.color565( 0xaa + row*0x20, 0x55 + row*0x10, 0 );
  224. // Draw position
  225. uint_fast16_t drawY = baseY + row*eqY*(EQ_BAR_HEIGHT+EQ_BAR_SPACING_Y);
  226. uint_fast8_t prevMeter = _meters[meterOffset+row];
  227. // Clamp the newMeters values to a sane range, please.
  228. // Avoid flicker at low volume levels.
  229. newMeters[row] = constrain( (uint_fast8_t)newMeters[row], (uint_fast8_t)1, (uint_fast8_t)MAX_EQ_BARS );
  230. // Gently fade out over time
  231. newMeters[row] = max( _meters[meterOffset+row]>>1, newMeters[row] );
  232. // Either draw or erase bricks, depending on whether the meter level increased or decreased
  233. boolean isErase = true;
  234. uint_fast16_t color = _bgColor; // default: erase
  235. uint_fast8_t drawHeight = EQ_BAR_HEIGHT;
  236. // If the energy of this row is higher: Set the draw color to something bright and colorful
  237. if( newMeters[row] > prevMeter ) {
  238. isErase = false;
  239. uint_fast8_t bright = 0x66 + row*0x33;
  240. color = tft.color565( 0x66, bright, bright );
  241. } else {
  242. // Be sure to erase the pseudo-3D sides
  243. drawHeight++;
  244. if( eqY==-1 ) drawY--;
  245. }
  246. uint_fast8_t minMeter = min( newMeters[row], prevMeter );
  247. uint_fast8_t maxMeter = max( newMeters[row], prevMeter );
  248. for( uint_fast8_t m=minMeter; m<maxMeter; m++ ) {
  249. uint_fast16_t drawX = baseX + m*eqX*(EQ_BAR_WIDTH+EQ_BAR_SPACING_X);
  250. tft.fillRect( drawX, drawY, EQ_BAR_WIDTH, drawHeight, color );
  251. // Draw fake 3D below each bar
  252. if( !isErase ) {
  253. tft.drawFastHLine( drawX, drawY+sideYOffset, EQ_BAR_WIDTH, sideColor );
  254. }
  255. }
  256. // Save this value for next draw cycle. Redraw minimum area.
  257. _meters[ meterOffset+row ] = newMeters[row];
  258. }
  259. // Advance _meters index for next batch of EQ meters
  260. meterOffset += 3;
  261. }
  262. }
  263. }
  264. #endif