// Two Thumbs (tT) Module
// twoThumbs.js
   console.log('TT version 2.50');
// 12:34 PM Fri February 26, 2021
// Written by: James D. Miller

/*

Dependencies:
   constructorsAndPrototypes.js (cP.)
   gwModule.js (gW.)
   utilities.js

*/

var twoThumbs = (function() {
   "use strict";
   
   // e.g. dF.drawCircle()
   var dF = new cP.DrawingFunctions();
   
   // Names starting with m_ indicate module-scope globals.
   var m_enabled = false;
   
   // module globals for things passed in by reference in initializeModule
   var m_clientCanvas_tt, m_ctx_tt, m_videoMirror, m_mK, m_cl_clientSide;
   
   // Grid of rectangles. UL (upper left corner), LR (lower right)
   var m_grid = {
      'jet_360':     {'active':false , 'mK_key':'w',   'UL':null, 'LR':null, 'dir_2d':null},
      'gun_360':     {'active':false , 'mK_key':'i',   'UL':null, 'LR':null, 'dir_2d':null},
      'shield':      {'active':false , 'mK_key':'sp',  'UL':null, 'LR':null},
      'color':       {'active':false , 'mK_key':'cl',  'UL':null, 'LR':null},
      
      'alt':         {'active':false , 'mK_key':null,  'UL':null, 'LR':null},
      
      // Controls that are dependent on the alt rectangle being touched.
      'esc':         {'active':false , 'mK_key':null,  'UL':null, 'LR':null},
      'demo7':       {'active':false , 'mK_key':'7',   'UL':null, 'LR':null},
      'demo8':       {'active':false , 'mK_key':'8',   'UL':null, 'LR':null},
      'freeze':      {'active':false , 'mK_key':'f',   'UL':null, 'LR':null},
      
      // Secondary control that fires the gun. Changes angle by controlling the rotation rate.
      'gun_scope':   {'active':false , 'mK_key':'ScTr','UL':null, 'LR':null, 'x_fraction':0}
   };
         
   // This is the same for both the jet and the gun.
   var m_statusDotRadius_fraction =  0.020;
   var m_statusDotRadius_px = null;
   
   // Control radius in units of screen fraction. The jet has four
   // strength levels: <1, >1 && <2, >2 && <3, >3.
   m_grid['jet_360'].cRadius_1_f = 0.050;  //0.090
   m_grid['jet_360'].cRadius_2_f = 0.090;  //0.130
   m_grid['jet_360'].cRadius_3_f = 0.130;  //0.170
   
   var m_jetRadiusColor_3 = "rgb(255,   0,   0)";
   var m_jetRadiusColor_2 = "rgb(200,   0,   0)";
   var m_jetRadiusColor_1 = "rgb(140,   0,   0)";
   var m_jetRadiusColor_0 = "rgb( 50,   0,   0)";
   
   // The gun has zero level, for bluffing. All touches outside that ring
   // are for firing.
   m_grid['gun_360'].cRadius_0_f = 0.040; //0.060
   
   var m_gunRadiusColor_0 = "rgb(255,   0,   0)";
   
   var m_bgColor = 'lightgray';
   var m_gridColor = '#232323'; // very dark gray // #008080 dark green
   
   // 0.10 uses 10% of the rectangle width for the dead spot.
   var m_scopeShootSpot = 0.20;
   
   var m_puckPopped = true;

   // Not yet using this adjustment point feature (TBD).
   //m_adjustmentPoint_2d = new cP.Vec2D(0, 0);
   
   ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
   ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
   
   function initializeModule( clientCanvas_tt, ctx_tt, videoMirror, mK, cl_clientSide) {
      
      m_clientCanvas_tt = clientCanvas_tt;
      m_ctx_tt = ctx_tt;
      m_videoMirror = videoMirror;
      m_mK = mK;
      m_cl_clientSide = cl_clientSide;
      
      m_clientCanvas_tt.style.borderColor = m_gridColor;
      updateAndDrawTouchGrid('updateOnly');
   }

   // Calculate point position in canvas coordinates as a function of fractional position.
   function absPos_x_px( fraction) {
      return Math.round(fraction * m_clientCanvas_tt.width);
   }
   
   function absPos_y_px( fraction) {
      return Math.round(fraction * m_clientCanvas_tt.height);
   }
   
   function resetRectangle( rectName) {
      // Update the target rectangle to reflect that there is no touch point in it.
      var rect = m_grid[ rectName];
      // The alt rectangle cases:
      if ((rectName =='esc' || rectName =='demo7' || rectName =='demo8' || rectName =='freeze')) {
         if (m_grid['alt'].active) {
            updateStatusDot( rectName, m_gridColor);
         } else {
            updateStatusDot( rectName, m_bgColor);
         }
      // The others...
      } else {
         if ((rectName == 'alt') || (rectName == 'shield')) {
            updateStatusDot( rectName, m_gridColor);
            
         } else if (rectName == 'color') {
            if (m_cl_clientSide.name) colorClientRect( hC.clientColor( m_cl_clientSide.name));
         
         } else if (rectName == 'jet_360') {
            updateStatusDot( rectName, m_gridColor);
            m_mK.jet_d = null; // jet angle in degrees
            
         } else if (rectName == 'gun_360') {
            updateStatusDot( rectName, m_gridColor);
            m_mK.gun_d = null; // gun angle in degrees
            
         } else if (rectName == 'gun_scope') {
            updateStatusDot( rectName, m_gridColor);
            // Scope Rotation Rate Fraction (ScRrf).
            m_mK.ScRrf = 0.00;
         }
      }
      // For all rectangles: deactivate and reset the primary mK attribute for that square.
      rect.active = false;
      if (rect.mK_key) m_mK[rect.mK_key] = 'U';
   }
   
   function processMultiTouch( touchVectors_2d_px) {
      for (var rectName in m_grid) {
         var rect = m_grid[ rectName];
         
         var atLeastOnePointInRect = false;
         for (var i = 0, len = touchVectors_2d_px.length; i < len; i++) {
            var p_2d = touchVectors_2d_px[i];
            
            if ( (p_2d.x > rect.UL.x) && (p_2d.x < rect.LR.x) && (p_2d.y > rect.UL.y) && (p_2d.y < rect.LR.y) ) {
               updateRectangle( rectName, p_2d);
               atLeastOnePointInRect = true;
               break;
            }
         }
         // If no touch point in this rectangle...
         if ( ! atLeastOnePointInRect) {
            resetRectangle( rectName);
         }
      }
      hC.handle_sending_mK_data( m_mK);
   }
   
   function processSingleTouchRelease( touchVector_2d_px) {
      for (var rectName in m_grid) {
         var rect = m_grid[ rectName];
         var p_2d = touchVector_2d_px;
         if ( (p_2d.x > rect.UL.x) && (p_2d.x < rect.LR.x) && (p_2d.y > rect.UL.y) && (p_2d.y < rect.LR.y) ) {
            
            resetRectangle( rectName);
            if (rectName == 'gun_scope') updateStatusDot('gun_360', m_gridColor, false);
            
            // When you release the alt rectangle, show as sleeping (not listening),
            // those rectangles that are dependent on the alt rectangle.
            if (rectName == 'alt') {
               updateStatusDot('esc',   m_bgColor);
               updateStatusDot('demo7', m_bgColor);
               updateStatusDot('demo8', m_bgColor);
               updateStatusDot('freeze',m_bgColor);
            }
            break;
         }
      }
   }
   
   function processGunAngleFromHost( data) {
      // Update the orientation vector
      var rect = m_grid['gun_360'];
      rect.dir_2d.set_angle( -data['gunAngle']);
      rect.endPoint_2d = rect.center_2d.add( rect.dir_2d);
      // Draw it
      updateStatusDot('gun_360', m_gridColor);
   }
   
   function processJetAngleFromHost( data) {
      // Update the orientation vector
      var rect = m_grid['jet_360'];
      // (also see updateRectangle)
      rect.dir_2d.set_angle(180 - data['jetAngle']);
      rect.endPoint_2d = rect.center_2d.subtract( rect.dir_2d);
      // Draw it
      updateStatusDot('jet_360', m_gridColor);
   }
   
   function updateStatusDot( rectName, statusDotColor, dotOnly = false) {
      var rect = m_grid[ rectName];
      
      // Draw a square over the prior status dot. This prevents a jagged edge when the status
      // dot is drawn. And is a little more efficient then drawing the whole rectangle for each update.
      // The eraser rectangle is a little larger than the dot.
      var extraPx = 1;  // old value = 3
      m_ctx_tt.fillStyle = m_bgColor;
      // upper left corner: x,y, width, height
      m_ctx_tt.fillRect( rect.center_2d.x - m_statusDotRadius_px - extraPx, rect.center_2d.y - m_statusDotRadius_px - extraPx, 
                       (m_statusDotRadius_px * 2) + extraPx*2, 
                       (m_statusDotRadius_px * 2) + extraPx*2);
      
      // Draw the dot.
      if (rectName == 'gun_scope') {
         var upperLeft_2d_px = rect.center_2d.subtract( new cP.Vec2D(m_statusDotRadius_px, m_statusDotRadius_px));
         dF.fillRectangle( m_ctx_tt, upperLeft_2d_px, {'width_px':m_statusDotRadius_px * 2, 'height_px':m_statusDotRadius_px * 1, 'fillColor':statusDotColor});   
      } else {
         dF.drawCircle( m_ctx_tt, rect.center_2d, {'radius_px':m_statusDotRadius_px, 'fillColor':statusDotColor} );
      }
      
      if ( ! dotOnly) {
         if ((rectName == 'jet_360') || (rectName == 'gun_360')) {
            // Draw the direction line.
            dF.drawLine( m_ctx_tt, rect.center_2d, rect.endPoint_2d, {'width_px':3, 'color':'white'} );
            
            // Draw nosecone
            if (rectName == 'jet_360') {
               var nose_cone_2d = rect.center_2d.add( rect.dir_2d.scaleBy(0.75));
               dF.drawCircle( m_ctx_tt, nose_cone_2d, {'fillColor': 'white', 'radius_px':absPos_x_px(0.005)} );
            }
         } else if (rectName == 'gun_scope') {
            // Draw line to indicate the rate of rotation
            if (Math.abs(rect.x_fraction) > m_scopeShootSpot) {
               var rotIndicator_start_2d = rect.center_2d.add( new cP.Vec2D(0,                                         -m_statusDotRadius_px * 0.5));
               var rotIndicator_end_2d   = rect.center_2d.add( new cP.Vec2D(m_statusDotRadius_px * rect.x_fraction, -m_statusDotRadius_px * 0.5));
               dF.drawLine( m_ctx_tt, rotIndicator_start_2d, rotIndicator_end_2d, {'width_px':m_statusDotRadius_px * 0.5, 'color':'black'} );
            }
         }
      }
   }
   
   function updateRectangle( rectName, point_2d) {
      var rect = m_grid[ rectName];
      var statusDotColor;
      
      var relativeToCenter_2d = point_2d.subtract( rect.center_2d);
      
      if (rectName == 'jet_360' || rectName == 'gun_360') {
         var rTC_lengthSquared = relativeToCenter_2d.length_squared();
         // Orient dir_2d to match the direction of relativeToCenter_2d
         // Note the negative sign correction (on the angle result) is necessary because of the 
         // negative orientation of the y axis with the screen (pixels) representation (not world here).
         var angle_d = -rect.dir_2d.matchAngle( relativeToCenter_2d);
         
         if (rectName == 'jet_360') {
            // Orient the tube in the opposite direction from the touch point.
            rect.endPoint_2d = rect.center_2d.subtract( rect.dir_2d);
            
            // Check where the point is relative to the control rings.
            
            // Always use at least the minimum jet power.
            statusDotColor = m_jetRadiusColor_0;
            m_mK.jet_t = 0.1; // Jet throttle
            
            // Stronger jet
            if (rTC_lengthSquared > Math.pow(m_grid['jet_360'].cRadius_1_px, 2)) {
               statusDotColor = m_jetRadiusColor_1;
               m_mK.jet_t = 0.4; 
               
               // Even stronger jet
               if (rTC_lengthSquared > Math.pow(m_grid['jet_360'].cRadius_2_px, 2)) {
                  statusDotColor = m_jetRadiusColor_2;
                  m_mK.jet_t = 0.7; 
                  
                  // Even stronger jet
                  if (rTC_lengthSquared > Math.pow(m_grid['jet_360'].cRadius_3_px, 2)) {
                     statusDotColor = m_jetRadiusColor_3;
                     m_mK.jet_t = 1.0;
                  }                  
               }
            }
            // Update mK for sending to the host.
            m_mK.w = 'D';
            m_mK.jet_d = angle_d + 180; // Use 180 to aim the nose cone; 0 to aim the jet tube
            
         } else if (rectName == 'gun_360') {
            // Orient the tube in the same direction from the touch point.
            rect.endPoint_2d = rect.center_2d.add( rect.dir_2d);
            
            // Check is the point is outside the control ring...
            if (rTC_lengthSquared > Math.pow(m_grid['gun_360'].cRadius_0_px, 2)) {
               statusDotColor = m_gunRadiusColor_0;
               m_mK.i = 'D';
            } else {
               statusDotColor = m_gridColor;
               m_mK.i = 'U';
            }
            // Update mK for sending to the host.
            m_mK.gun_d = angle_d;
         }
         updateStatusDot( rectName, statusDotColor);
        
      } else if (rectName == 'shield') {
         if (rect.mK_key) m_mK[rect.mK_key] = 'D';
         updateStatusDot( rectName, 'yellow');
         
      } else if (rectName == 'color') {
         //           m_mK.cl       = 'D'
         if (rect.mK_key) m_mK[rect.mK_key] = 'D';
         colorClientRect( m_bgColor);
         
      } else if (rectName == 'gun_scope') {
         // Rotation rate fraction (Rrf) for the scope control (Sc), where x_fraction varies 
         // from -1 to +1;
         rect.x_fraction = relativeToCenter_2d.x / ((rect.LR.x - rect.UL.x)/2.0);
         var x_fraction_abs = Math.abs( rect.x_fraction);
         if (x_fraction_abs > 0) {
            var x_fraction_sign = rect.x_fraction / x_fraction_abs;
         } else {
            var x_fraction_sign = 1.0;
         }
         
         // Shooting spot in the middle where it will only shoot, not rotate.
         if (x_fraction_abs < m_scopeShootSpot) {
            var x_fraction_mapped = 0.00;
            m_mK[rect.mK_key] = 'D';
            updateStatusDot( rectName, 'red');
            updateStatusDot('gun_360', 'red', false);
         
         // The outer areas will only rotate, not shoot.
         } else {
            // Map the x_fraction value so that near the edge of the dead zone, the rate is small. At the
            // outer edge of the rectangle, the rate is 1.0 times the normal keyboard rotation rate.
            var x_fraction_mapped = x_fraction_sign * (x_fraction_abs - m_scopeShootSpot - 0.01) * 1.0;
            m_mK[rect.mK_key] = 'U';
            updateStatusDot( rectName, 'yellow');
         }
         // Scope Rotation Rate Fraction (ScRrf)
         m_mK['ScRrf'] = x_fraction_mapped.toFixed(2);
         
      } else if (rectName == 'alt') {
         updateStatusDot( rectName, 'yellow');
         // Show the alt-dependent rectangles as awake (ready to receive a touch). Don't do this
         // if the alt rectangle is already active. This check is necessary to allow the alt keys
         // to show yellow after they are touched. Remember, the updateRectangle function fires
         // twice when using the alt feature.
         if (!m_grid['alt'].active) {
            updateStatusDot('esc',   m_gridColor);
            updateStatusDot('demo7', m_gridColor);
            updateStatusDot('demo8', m_gridColor);
            updateStatusDot('freeze',m_gridColor);
         }
      
      // Must use the alt button for these:
      } else if (m_grid['alt'].active && (rectName =='esc' || rectName =='demo7' || rectName =='demo8' || rectName =='freeze')) {
         if (rectName =='esc') {
            m_clientCanvas_tt.width  = m_videoMirror.width;
            m_clientCanvas_tt.height = m_videoMirror.height;
            // Note: the alt and esc rectangles get "released" in this call to changeDisplay.
            changeDisplay('exit');
            return;
         }
         if (rect.mK_key) m_mK[rect.mK_key] = 'D';
         updateStatusDot( rectName, 'yellow');
      }
      
      // No matter what, set this rectangle to be active.
      rect.active = true;
   }
   
   // Color the rectangle that indicates the client color.
   function colorClientRect( color) {
      // Draw this a little smaller than the actual rectangle.
      var shrink_px = 8;
      var ULx = m_grid['color'].UL.x + shrink_px;
      var ULy = m_grid['color'].UL.y + shrink_px;
      var LRx = m_grid['color'].LR.x - shrink_px;
      var LRy = m_grid['color'].LR.y - shrink_px;
      
      var width_px = LRx - ULx;
      var height_px = LRy - ULy;
      
      m_ctx_tt.fillStyle = color;
      m_ctx_tt.fillRect(ULx, ULy, width_px, height_px);
      
      // Circle to reflect how it looks on the host
      if ((color == m_bgColor) && ( ! m_puckPopped)) {
         dF.drawCircle( m_ctx_tt, m_grid['color'].center_2d, 
            {'radius_px':m_statusDotRadius_px * 2.0, 'fillColor':hC.clientColor( m_cl_clientSide.name)} );    //2.5 old value before Pixel 4a 5G
      }
      
      // Add circle to indicate that you still have a puck to drive.
      if ( ! m_puckPopped) {
         dF.drawCircle( m_ctx_tt, m_grid['color'].center_2d, {'radius_px':m_statusDotRadius_px, 'fillColor':m_gridColor} );
      }
   }
   
   function scaledFont( fontInt) {
      var myFudge = 1.5;
      var scaledFontValue = myFudge * fontInt * (window.innerWidth * window.devicePixelRatio / 1920);
      return scaledFontValue.toFixed(1) + "px Arial";
   }
   
   function updateAndDrawTouchGrid( mode) {      
      m_ctx_tt.fillStyle = m_bgColor;
      m_ctx_tt.fillRect(0,0, m_clientCanvas_tt.width, m_clientCanvas_tt.height);
      
      //m_adjustmentPoint_2d.x = absPos_x_px( 0.47);
      //m_adjustmentPoint_2d.y = absPos_y_px( 0.90);
      
      m_statusDotRadius_px = absPos_x_px( m_statusDotRadius_fraction);
      
      m_grid['jet_360'].cRadius_1_px = absPos_x_px(m_grid['jet_360'].cRadius_1_f);
      m_grid['jet_360'].cRadius_2_px = absPos_x_px(m_grid['jet_360'].cRadius_2_f);
      m_grid['jet_360'].cRadius_3_px = absPos_x_px(m_grid['jet_360'].cRadius_3_f);
      
      m_grid['gun_360'].cRadius_0_px = absPos_x_px(m_grid['gun_360'].cRadius_0_f);
      
      // x position of the vertical lines (from left to right).
      var x0  = absPos_x_px( 0.00);
      var x0a = absPos_x_px( 0.10);
      var x0b = absPos_x_px( 0.20);
      var x0c = absPos_x_px( 0.30);
      var x0d = absPos_x_px( 0.315);
      var x0e = absPos_x_px( 0.455);
      
      var x1 = absPos_x_px( 0.47);
      var x2 = absPos_x_px( 0.60);    
      var x3 = absPos_x_px( 1.00);
      // Center +/- the half width of the scope spot.
      var x2a = (x3 + x2)/2.0 - ((x3-x2) * m_scopeShootSpot/2.0);
      var x2b = (x3 + x2)/2.0 + ((x3-x2) * m_scopeShootSpot/2.0);
      
      // y position of the horizontal lines (from top to bottom).
      var y0  = absPos_y_px( 0.00);
      var y0a = absPos_y_px( 0.65);
      var y0b = absPos_y_px( 0.85);
      var y1  = absPos_y_px( 0.90);
      var y2  = absPos_y_px( 1.00);
      
      // Define all the rectangles in the grid. UL: upper left, LR: lower right.
      m_grid['jet_360'].UL = new cP.Vec2D(x0, y0);
      m_grid['jet_360'].LR = new cP.Vec2D(x1, y1);
      m_grid['jet_360'].dir_2d = new cP.Vec2D(0, -m_statusDotRadius_px); // as if touch point is high
         
      m_grid['gun_360'].UL = new cP.Vec2D(x2, y0);
      m_grid['gun_360'].LR = new cP.Vec2D(x3, y0b);
      m_grid['gun_360'].dir_2d = new cP.Vec2D(0, -m_statusDotRadius_px); // as if touch point is high
      
      m_grid['shield'].UL = new cP.Vec2D(x1, y0);
      m_grid['shield'].LR = new cP.Vec2D(x2, y0a);
      
      m_grid['color'].UL = new cP.Vec2D(x1, y0a);
      m_grid['color'].LR = new cP.Vec2D(x2, y0b);
      
      m_grid['freeze'].UL = new cP.Vec2D(x0, y1);
      m_grid['freeze'].LR = new cP.Vec2D(x0a, y2);
      
      m_grid['demo7'].UL = new cP.Vec2D(x0a, y1);
      m_grid['demo7'].LR = new cP.Vec2D(x0b, y2);
      
      m_grid['demo8'].UL = new cP.Vec2D(x0b, y1);
      m_grid['demo8'].LR = new cP.Vec2D(x0c, y2);
      
      m_grid['esc'].UL = new cP.Vec2D(x0d, y1);
      m_grid['esc'].LR = new cP.Vec2D(x0e, y2);
      
      m_grid['alt'].UL = new cP.Vec2D(x1, y1);
      m_grid['alt'].LR = new cP.Vec2D(x2, y2);
         
      m_grid['gun_scope'].UL = new cP.Vec2D(x2, y0b);
      m_grid['gun_scope'].LR = new cP.Vec2D(x3, y2);
      
      // Calculate the center point of each rectangle.
      for (var rectName in m_grid) {
         var rect = m_grid[ rectName];
         rect.center_2d = rect.UL.add( rect.LR).scaleBy(1.0/2.0);
         if ((rectName == "jet_360") || (rectName == "gun_360")) {
            // endPoint calculations for jet and gun
            if (rectName == 'jet_360') {
               // This endPoint calc will orient the jet tube in the opposite direction from the initial direction of dir_2d.
               rect.endPoint_2d = rect.center_2d.subtract( rect.dir_2d);
            } else if (rectName == 'gun_360') {
               // Orient gun tube in same direction as dir_2d
               rect.endPoint_2d = rect.center_2d.add( rect.dir_2d);
            }
         }
      }
      
      if (mode == 'draw') {
         // Draw grid...
         // Vertical lines
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x0a, y1), new cP.Vec2D(x0a, y2), {'width_px':3, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x0b, y1), new cP.Vec2D(x0b, y2), {'width_px':3, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x0c, y1), new cP.Vec2D(x0c, y2), {'width_px':3, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x0d, y1), new cP.Vec2D(x0d, y2), {'width_px':3, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x0e, y1), new cP.Vec2D(x0e, y2), {'width_px':3, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x1, y0),  new cP.Vec2D(x1, y2),  {'width_px':5, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x2, y0),  new cP.Vec2D(x2, y2),  {'width_px':5, 'color':m_gridColor});
         
         // Vertical lines in the scope rectangle
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x2a, y0b), new cP.Vec2D(x2a, y2), {'width_px':1, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x2b, y0b), new cP.Vec2D(x2b, y2), {'width_px':1, 'color':m_gridColor});
         
         // Draw the vertical gradient lines in the scope rectangle.
         var width_px = x2a - x2;
         var step_px = Math.round(width_px/10.0);
         var length_px = Math.round((y2 - y0b)/3.0);
         for (var i = step_px; i < width_px; i += step_px) {
            dF.drawLine( m_ctx_tt, new cP.Vec2D(x2 + i, y2-length_px), new cP.Vec2D(x2 + i, y2), {'width_px':1, 'color':m_gridColor});
            dF.drawLine( m_ctx_tt, new cP.Vec2D(x3 - i, y2-length_px), new cP.Vec2D(x3 - i, y2), {'width_px':1, 'color':m_gridColor});
            step_px *= 0.91; // reduce the step
            if (step_px < 1) step_px = 1;
         }
         
         // Horizontal lines
         // First two run only the width of the shield rectangle.
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x1, y0a), new cP.Vec2D(x2, y0a), {'width_px':5, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x1, y0b), new cP.Vec2D(x2, y0b), {'width_px':5, 'color':m_gridColor});
         // The next pair do the main bottom line: second segment is at a higher y level for the scope rectangle.
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x0, y1),  new cP.Vec2D(x2, y1),  {'width_px':5, 'color':m_gridColor});
         dF.drawLine( m_ctx_tt, new cP.Vec2D(x2, y0b), new cP.Vec2D(x3, y0b), {'width_px':5, 'color':m_gridColor});
         
         // Adjustment Point
         //dF.drawCircle( m_ctx_tt, m_adjustmentPoint_2d, {'fillColor': 'red', 'radius_px':5} );
         
         // Status dots
         updateStatusDot('jet_360',   m_gridColor);
         updateStatusDot('gun_360',   m_gridColor);
         updateStatusDot('shield',    m_gridColor);
         updateStatusDot('alt',       m_gridColor);
         updateStatusDot('gun_scope', m_gridColor);
         
         // Control ring
         dF.drawCircle( m_ctx_tt, m_grid['jet_360'].center_2d, 
            {'fillColor':'noFill', 'radius_px':m_grid['jet_360'].cRadius_1_px, 'borderWidth_px':3, 'borderColor':m_jetRadiusColor_1} );
         dF.drawCircle( m_ctx_tt, m_grid['jet_360'].center_2d, 
            {'fillColor':'noFill', 'radius_px':m_grid['jet_360'].cRadius_2_px, 'borderWidth_px':3, 'borderColor':m_jetRadiusColor_2} );
         dF.drawCircle( m_ctx_tt, m_grid['jet_360'].center_2d, 
            {'fillColor':'noFill', 'radius_px':m_grid['jet_360'].cRadius_3_px, 'borderWidth_px':3, 'borderColor':m_jetRadiusColor_3} );
         
         dF.drawCircle( m_ctx_tt, m_grid['gun_360'].center_2d,
            {'fillColor':'noFill', 'radius_px':m_grid['gun_360'].cRadius_0_px, 'borderWidth_px':3, 'borderColor':m_gunRadiusColor_0} );
         
         /*
         // Draw some test lines for use in positioning text on the grid (1% by 1% grid cells).
         for (var i = 0; i < 100; i++) {
            // Vertical lines in 1% steps of x range.
            var x_test_px = x0 + absPos_x_px( i * 0.01);
            dF.drawLine( m_ctx_tt, new cP.Vec2D(x_test_px, y0), new cP.Vec2D(x_test_px, y2), {'width_px':1, 'color':m_gridColor});
            
            // Horizontal lines in 1% steps of y range (remember y increased going down the screen)
            var y_test_px = y0 + absPos_y_px( i * 0.01);
            dF.drawLine( m_ctx_tt, new cP.Vec2D(x0, y_test_px), new cP.Vec2D(x3, y_test_px), {'width_px':1, 'color':m_gridColor});
         }
         */
         
         // Text labels
         m_ctx_tt.font = scaledFont(25); //25px Arial
         m_ctx_tt.fillStyle = m_gridColor;
         
         // Set the location on this text using fractional positions. These x,y coordinates specify the position of the lower left corner of the text string.
         m_ctx_tt.fillText('jet',     m_grid['jet_360'].UL.x   + absPos_x_px(0.020),  m_grid['jet_360'].UL.y   + absPos_y_px( 0.06));
         m_ctx_tt.fillText('shooter', m_grid['gun_360'].UL.x   + absPos_x_px(0.020),  m_grid['gun_360'].UL.y   + absPos_y_px( 0.06));
         m_ctx_tt.fillText('scope',   m_grid['gun_scope'].UL.x + absPos_x_px(0.020),  m_grid['gun_scope'].UL.y - absPos_y_px( 0.03));
         
         m_ctx_tt.fillText('shield',  m_grid['shield'].UL.x    + absPos_x_px(0.022),  m_grid['shield'].UL.y    + absPos_y_px( 0.06));
         
         m_ctx_tt.font = scaledFont(20); //20px Arial
         m_ctx_tt.fillText('7',       m_grid['demo7'].UL.x     + absPos_x_px(0.005),  m_grid['demo7'].UL.y     + absPos_y_px( 0.05));
         m_ctx_tt.fillText('8',       m_grid['demo8'].UL.x     + absPos_x_px(0.005),  m_grid['demo8'].UL.y     + absPos_y_px( 0.05));
         m_ctx_tt.fillText('f',       m_grid['freeze'].UL.x    + absPos_x_px(0.005),  m_grid['freeze'].UL.y    + absPos_y_px( 0.05));
         
         m_ctx_tt.font = scaledFont(19); //19px Arial
         m_ctx_tt.fillText('esc',     m_grid['esc'].UL.x       + absPos_x_px(0.005),  m_grid['esc'].UL.y       + absPos_y_px( 0.05));
         m_ctx_tt.fillText('alt',     m_grid['alt'].UL.x       + absPos_x_px(0.005),  m_grid['alt'].UL.y       + absPos_y_px( 0.05));
         m_ctx_tt.fillText('ccw',     x2a                         - absPos_x_px(0.052),  m_grid['gun_scope'].UL.y + absPos_y_px( 0.070));
         m_ctx_tt.fillText('cw',      x2b                         + absPos_x_px(0.009),  m_grid['gun_scope'].UL.y + absPos_y_px( 0.070));
         
         if (m_cl_clientSide.name) colorClientRect( hC.clientColor( m_cl_clientSide.name));
      }
   }
   
   // Function supporting full-screen display mode
   function changeDisplay( mode) {
      if ((mode == 'fullScreen') || (mode == 'normal')) {
         
         if (window.innerWidth < window.innerHeight) {
            var orientationMessage = "The Two-Thumbs client requests that your phone be oriented for landscape viewing. Please turn it sideways, then try touching the Two-Thumbs button again."
            alert( orientationMessage);
            displayMessage( orientationMessage);
            return;
         }
         
         m_enabled = true;
         hC.sendSocketControlMessage( {'from':m_cl_clientSide.name, 'to':'host', 'data':{'twoThumbsEnabled':{'value':true}} } );
         
         // If there's a stream active, shut it down.
         if (chkRequestStream.checked) {
            chkRequestStream.click();
         }
         // Reveal the twoThumbs canvas.
         m_clientCanvas_tt.removeAttribute("hidden");
         // Hide the video streaming element.
         m_videoMirror.setAttribute("hidden", null);

         if (mode == 'fullScreen') {
            var dpr = window.devicePixelRatio;
            //console.log('dpr='+dpr);
            // write to chat area
            //displayMessage('dpr1='+dpr); // for android debugging...
            hC.changeFullScreenMode( m_clientCanvas_tt, 'on');
            
            if (dpr <= 1.25) {
               var scalingFactor = 1.0; // my laptop
            } else if ((dpr > 1.25) && (dpr <= 2.6)) {
               var scalingFactor = dpr * 0.6; // my moto 0.6
            } else if (dpr > 2.6) {
               var scalingFactor = dpr * 0.7; // my Pixel 4a 5G
            }
            
            // A larger delay is needed with FireFox. Some delay is also needed with
            // Chrome on Android. Both seem to work fine at 600ms. So for now...
            var userAgent = window.navigator.userAgent;
            if (userAgent.includes("Firefox")) {
               var waitForFullScreen = 600;
               console.log("firefox detected");
            } else {
               var waitForFullScreen = 600;
            }
         
            // Delay (to wait for fullscreen change to finish) is needed.
            window.setTimeout(function() {
               m_clientCanvas_tt.width  = window.innerWidth  * scalingFactor;
               m_clientCanvas_tt.height = window.innerHeight * scalingFactor; 
               //m_ctx_tt.scale(dpr, dpr); // played with this, but not useful here...
            }, waitForFullScreen);
            
            // Wait until a little after the canvas-resize delay above.
            // Notice "this" context is passed in with bind.
            window.setTimeout(function() {
               updateAndDrawTouchGrid('draw');
               
               // Check with the host to see if there is a puck for this client. (Note, of course, that gW.clients of the hosts browser window is
               // not accessible here in the client window.) This will also sync the angles of the jet and gun tubes on the client.
               hC.sendSocketControlMessage( {'from':m_cl_clientSide.name, 'to':'host', 'data':{'puckPopped':{'value':'probeAtHost'}} } );
               
            }.bind(this), 700);
            
         } else if (mode == 'normal') {
            updateAndDrawTouchGrid('draw');
         }
         
      } else if (mode == 'exit') {
         if (document.fullscreenElement) hC.changeFullScreenMode( m_clientCanvas_tt, 'off');
         
         // De-activate the two rectangles that may have gotten you in here (remember, you didn't have to lift
         // your fingers). This effectively resets these rectangles (like releasing your touch).
         m_grid['esc'].active = false;
         m_grid['alt'].active = false;
         
         // Reveal the video element.
         if (hC.getClientDeviceType() == "desktop") m_videoMirror.removeAttribute("hidden");
         // Hide the two thumbs canvas.
         m_clientCanvas_tt.setAttribute("hidden", null);
         
         chkTwoThumbs.checked = false;
         m_enabled = false;
         hC.sendSocketControlMessage( {'from':m_cl_clientSide.name, 'to':'host', 'data':{'twoThumbsEnabled':{'value':false}} } );
         hC.initialize_mK(); // clear out the TT parameters like thrust, as you shift back to keyboard play.
      }
   }

   // see comments before the "return" section of gwModule.js
   return {
      setPuckPopped: function( val) { m_puckPopped = val; },
      getEnabled: function() { return m_enabled; },
      setEnabled: function( val) { m_enabled = val; },
      
      initializeModule: initializeModule,
      processGunAngleFromHost: processGunAngleFromHost,
      processJetAngleFromHost: processJetAngleFromHost,
      colorClientRect: colorClientRect,
      processMultiTouch: processMultiTouch,
      processSingleTouchRelease: processSingleTouchRelease,
      changeDisplay: changeDisplay
   };

})();