/*
Copyright 2022 James D. Miller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Two Thumbs (twoThumbs) module
// twoThumbs.js
console.log('TT version 0.0');
// 8:25 PM Wed February 2, 2022
/*
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);
// 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 drawFatFingerHelper( rectName, onOff) {
let rect = m_grid[ rectName];
let rectHeight_px = (rect.LR.y - rect.UL.y);
let rectWidth_px = (rect.LR.x - rect.UL.x);
let arcRadius_px = (rectWidth_px / 2) - 5;
// the background rectangle (an eraser)
let upperLeft_2d_px = rect.UL.add( new cP.Vec2D( 0, -(rectHeight_px + 5) ));
dF.fillRectangle( m_ctx_tt, upperLeft_2d_px, {'width_px':rectWidth_px, 'height_px':(rectHeight_px - 0), 'fillColor':m_bgColor});
// the arc (eyebrow)
if (onOff == "on") {
let fillColor = (rectName == "freeze") ? "red" : "green";
m_ctx_tt.fillStyle = fillColor;
m_ctx_tt.beginPath();
m_ctx_tt.arc( rect.center_2d.x, rect.UL.y + 10, arcRadius_px, 1.1 * Math.PI, 1.9 * Math.PI, false);
m_ctx_tt.fill();
}
}
function updateStatusDot( rectName, statusDotColor) {
var rect = m_grid[ rectName];
// Draw an arc above these small rectangles to give better feedback of a successful touch.
if (['demo7','demo8','freeze'].includes( rectName)) {
if ((statusDotColor == m_bgColor) || (statusDotColor == m_gridColor)) {
drawFatFingerHelper( rectName, 'off');
} else {
drawFatFingerHelper( rectName, 'on');
}
}
// 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 gun-scope rotation rate indicator (it's kind of a rectangular status dot, sort of..)
if (rectName == 'gun_scope') {
// background rectangle
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});
// draw a thick 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'} );
}
// Draw the status dot
} else {
dF.drawCircle( m_ctx_tt, rect.center_2d, {'radius_px':m_statusDotRadius_px, 'fillColor':statusDotColor} );
// extra features for the dot
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)} );
}
}
}
}
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');
// 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.085), m_grid['freeze'].UL.y + absPos_y_px( 0.05)); //0.005 aligned to the left...
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
};
})();