// Two Thumbs (twoThumbs) module
// twoThumbs.js
console.log('TT version 0.0');
// 8:25 PM Wed February 2, 2022
// 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 {
// Variables
setPuckPopped: function( val) { m_puckPopped = val; },
getEnabled: function() { return m_enabled; },
setEnabled: function( val) { m_enabled = val; },
// Methods
initializeModule: initializeModule,
processGunAngleFromHost: processGunAngleFromHost,
processJetAngleFromHost: processJetAngleFromHost,
colorClientRect: colorClientRect,
processMultiTouch: processMultiTouch,
processSingleTouchRelease: processSingleTouchRelease,
changeDisplay: changeDisplay
};
})();