// Game Window (gW) Module // Version 1.17 (11:12 PM Thu June 15, 2017) // Written by: James D. Miller // The demos, and their multi-player functionality, are dependent on two additional // JavaScript modules: hostAndClient.js and server.js. Discussion and links to these files // are at http://www.timetocode.org/multiplayer.html var gW = (function () { // To insist on non-sloppy code: e.g. globals, etc... "use strict"; // Short names for Box2D functions var b2Vec2 = Box2D.Common.Math.b2Vec2 , b2BodyDef = Box2D.Dynamics.b2BodyDef , b2Body = Box2D.Dynamics.b2Body , b2FixtureDef = Box2D.Dynamics.b2FixtureDef , b2Fixture = Box2D.Dynamics.b2Fixture , b2World = Box2D.Dynamics.b2World , b2DebugDraw = Box2D.Dynamics.b2DebugDraw , b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef , b2MassData = Box2D.Collision.Shapes.b2MassData , b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape , b2CircleShape = Box2D.Collision.Shapes.b2CircleShape , b2AABB = Box2D.Collision.b2AABB ; // Common variables inside of gW (game window) ////////////////////////////// var g_2d_mps2, g_mps2 = 9.8; // The Air Table (aT): a place to call home for pucks, pins, springs, and walls. var aT = {}; aT.multiSelectMap = {}; aT.puckMap = {}; // keyed by puck name. aT.jelloPucks = []; // An array for use in testing for tangled jello. aT.pinMap = {}; // keyed by pin name. aT.springMap = {}; // keyed by spring name. aT.wallMap = {}; // keyed by wall name. // Make a separate container for constants (c) used by aT objects. This avoids // circular references (and associated problems with JSON capturing). var c = {}; c.restitution_default_gOn = 0.7; c.friction_default_gOn = 0.6; c.restitution_default_gOff = 1.0; c.friction_default_gOff = 0.1; c.g_ON = false; // Seconds per frame (at page load) c.deltaT_s = 1.0/60.0; c.dtFloating = false; c.demoIndex = null; //c.contactCounter = 0; c.tangleTimer_s = 0; // Client map keyed by client name. var clients = {}; var tableMap = new Map(); // Special map where keys can be objects. var world, worldAABB; var myRequest, time_previous, dt_frame_ms, dt_frame_previous_ms, dt_frame_s, resumingAfterPause; var canvas, ctx, px_per_m; // Document Controls (dC). var dC = {}; dC.gravity = null; dC.pause = null; dC.comSelection = null; dC.multiplayer = null; dC.editor = null; // Key values. var keyMap = {'66':'key_b', '67':'key_c', '70':'key_f', '71':'key_g', '77':'key_m', '80':'key_p', '69':'key_e', '65':'key_a', '83':'key_s', '68':'key_d', '87':'key_w', '74':'key_j', '75':'key_k', '76':'key_l', '73':'key_i', '16':'key_shift', '82':'key_r', '84':'key_t', '86':'key_v', '88':'key_x', '90':'key_z', '37':'key_leftArrow', '39':'key_rightArrow', '38':'key_upArrow', '40':'key_downArrow', '17':'key_ctrl', '32':'key_space'}; ///////////////////////////////////////////////////////////////////////////// //// //// Object Prototypes //// ///////////////////////////////////////////////////////////////////////////// function Vec2D(x, y) { this.x = x; this.y = y; } Vec2D.prototype.addTo = function( vectorToAdd) { // Modify the base vector. this.x += vectorToAdd.x; this.y += vectorToAdd.y; } Vec2D.prototype.add = function( vectorToAdd) { // Return a new vector. var x_sum = this.x + vectorToAdd.x; var y_sum = this.y + vectorToAdd.y; return new Vec2D( x_sum, y_sum); } Vec2D.prototype.subtract = function( vectorToSubtract) { // Return a new vector. var x_diff = this.x - vectorToSubtract.x; var y_diff = this.y - vectorToSubtract.y; return new Vec2D( x_diff, y_diff); } Vec2D.prototype.scaleBy = function( scalingFactor) { var x_prod = this.x * scalingFactor; var y_prod = this.y * scalingFactor; return new Vec2D( x_prod, y_prod); } Vec2D.prototype.length = function() { return Math.sqrt(this.x*this.x + this.y*this.y); } Vec2D.prototype.dot = function( vector) { return (this.x * vector.x) + (this.y * vector.y); } Vec2D.prototype.projection_onto = function( vec_B) { var vB_dot_vB = vec_B.dot( vec_B); if (vB_dot_vB > 0) { return vec_B.scaleBy( this.dot( vec_B) / vB_dot_vB ); } else { // Must catch this null when dealing with pinned springs (can have // zero separation) return null; } } Vec2D.prototype.rotate90 = function() { return new Vec2D(-this.y, this.x); } Vec2D.prototype.rotated_by = function( angle_degrees) { var angle_radians = (Math.PI/180) * angle_degrees; var cos = Math.cos( angle_radians); var sin = Math.sin( angle_radians); // The rotation transformation. var x = this.x * cos - this.y * sin; var y = this.x * sin + this.y * cos; // Modify the original vector. this.x = x; this.y = y; } Vec2D.prototype.length_squared = function() { return (this.x*this.x + this.y*this.y); } Vec2D.prototype.get_angle = function() { // Determine the angle that this vector makes with the x axis. Measure // counterclockwise from the x axis. if (this.length_squared() == 0) { return 0; } else { return Math.atan2(this.y, this.x) * (180/Math.PI); } } Vec2D.prototype.set_angle = function( angle_degrees) { // Set the direction of the vector to a specific angle. this.x = this.length(); this.y = 0; this.rotated_by( angle_degrees); } function Client( pars) { this.color = pars.color || "red"; this.name = pars.name || "manWithNoName"; this.puck = null; this.isMouseDown = false; this.button = null; this.mouseX_px = 10; this.mouseY_px = 10; this.mouse_2d_px = new Vec2D(this.mouseX_px, this.mouseY_px); this.mouse_2d_m = worldFromScreen( this.mouse_2d_px); this.selectedBody = null; // Selection point (in the local coordinate system of the selected object). this.selectionPoint_l_2d_m = null; // Selection point (in the world coordinates). this.selectionPoint_w_2d_m = null; this.selectionPoint_w_2d_px = null; // Initialize all the key values to be Up. for (var key in keyMap) this[keyMap[key]] = 'U'; // The following enable/disable feature is needed for keys that do something that // should only be done once while the key is down. Examples where this is NOT needed // are the tube rotation keys. In those cases, something must be // done in each frame while the key is down. this.key_f_enabled = true; // Freeze all the pucks. this.key_s_enabled = true; // Flip the jet. this.key_k_enabled = true; // Change the gun orientation by 1 large increment. this.key_i_enabled = true; // Start a bullet stream. // This client-cursor triangle is oriented like an arrow pointing to 10 o'clock. //this.triangle_raw_2d_px = [new Vec2D(0,0), new Vec2D(14,8), new Vec2D(8,14)]; this.triangle_raw_2d_px = [new Vec2D(0,0), new Vec2D(11,12), new Vec2D(3,16)]; } // Variables common to all instances of Client... Client.mouse_strings = {'0':{'c_drag': 2.0, 'k_Npm': 60.0}, '1':{'c_drag': 0.1, 'k_Npm': 2.0}, '2':{'c_drag': 20.0, 'k_Npm': 1000.0}}; Client.colors = {'1':'yellow','2':'blue','3':'green','4':'pink','5':'orange', '6':'brown','7':'greenyellow','8':'cyan','9':'tan','0':'gray'}; Client.prototype.checkForMouseSelection = function() { // Deal with selection. if (this.selectedBody === null) { if (this.isMouseDown) { // Check for body at the mouse position. var selected_b2d_Body = b2d_getBodyAt( this.mouse_2d_m); if (selected_b2d_Body) { // Block the selection on kinematic bodies (like walls and pins) by a network client or if the editor is off. if ( (selected_b2d_Body.GetType() == b2Body.b2_kinematicBody) && ((this.name != 'local') || (!dC.editor.checked)) ) { selected_b2d_Body = null; } else { // Consider the special case where local client is trying to edit multiple objects (shift key is down). if ((this.name == 'local') && (this.key_shift == "D")) { var selectedBody = tableMap.get( selected_b2d_Body); // Add this body to the multiple-select map (if not already there). if (!(selectedBody.name in aT.multiSelectMap) && (this.button == 0)) { aT.multiSelectMap[ selectedBody.name] = selectedBody; console.log(Object.keys(aT.multiSelectMap).length + ', ' + selectedBody.name); // Remove this body from the map. } else if ((selectedBody.name in aT.multiSelectMap) && (this.button == 2)) { // un-dash the springs Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = false; }); delete aT.multiSelectMap[ selectedBody.name]; // re-dash the springs Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = true; }); } // Normal selection: } else { // Which body object has been selected? this.selectedBody = tableMap.get( selected_b2d_Body); // Mark it as selected and record the local point. this.selectionPoint_l_2d_m = selected_b2d_Body.GetLocalPoint( this.mouse_2d_m); } } } } } else { // Released the mouse button: if (!this.isMouseDown) { this.selectionPoint_l_2d_m = null; this.selectionPoint_w_2d_m = null; this.selectionPoint_w_2d_px = null; this.selectedBody = null; } } } Client.prototype.updateSelectionPoint = function() { // Calculate (update) the world location of the selection point (for use in force calculations) if (dC.comSelection.checked) { this.selectionPoint_w_2d_m = this.selectedBody.position_2d_m; } else { // Convert the local selection-point vector to a world vector. this.selectionPoint_w_2d_m = Vec2D_from_b2Vec2( this.selectedBody.b2d.GetWorldPoint( this.selectionPoint_l_2d_m)); } } Client.prototype.calc_string_forces_on_puck = function() { // Calculate the forces. var stretch_2d_m = this.mouse_2d_m.subtract( this.selectionPoint_w_2d_m); var spring_force_2d_N = stretch_2d_m.scaleBy( Client.mouse_strings[this.button]['k_Npm']); //this.selectedBody.cursorString_spring_force_2d_N.addTo( spring_force_2d_N); // Add this force and application point to the nonCOM force array. this.selectedBody.nonCOM_2d_N.push({force_2d_N: spring_force_2d_N, point_w_2d_m: this.selectionPoint_w_2d_m}); //console.log("v.x=" + this.selectedBody.velocity_2d_mps.x); var drag_force_2d_m = this.selectedBody.velocity_2d_mps.scaleBy( -1 * Client.mouse_strings[this.button]['c_drag']); //this.selectedBody.cursorString_puckDrag_force_2d_N.addTo( drag_force_2d_m); // Add a force,point object to the nonCOM array. this.selectedBody.nonCOM_2d_N.push({force_2d_N: drag_force_2d_m, point_w_2d_m: this.selectionPoint_w_2d_m}); } Client.prototype.moveToCursorPosition = function() { // For manipulating kinematic objects (walls and pins) if (dC.comSelection.checked) { // If COM selection, simply put the object at the mouse position. var newPosition = this.mouse_2d_m; } else { // If not COM selection, calculate the world delta between the current mouse position and the original selection point. // This delta is useful for positioning (dragging) a kinematic body (like a wall) so that it's selection point // follows the moving mouse location. var deltaForKinematicBodies_w_2d_m = this.mouse_2d_m.subtract( this.selectionPoint_w_2d_m); // Adding the delta to the body position, moves the body so that the original selection point is at the mouse position. var newPosition = this.selectedBody.position_2d_m.add( deltaForKinematicBodies_w_2d_m); } this.selectedBody.position_2d_m = newPosition; this.selectedBody.position_2d_px = screenFromWorld( this.selectedBody.position_2d_m); this.selectedBody.b2d.SetPosition( newPosition); } Client.prototype.drawCursor = function() { // Draw a cursor the for the network clients. //this.drawCircle( this.mouse_2d_px); // Draw a triangle for the network client's cursor. // Before you can draw it, you have to know where it is on the screen. this.triangle_2d_px = []; var offset_2d_px = new Vec2D(2,2); //tweak the positioning of the cursor. for (var i = 0, len = this.triangle_raw_2d_px.length; i < len; i++) { // Put it at the mouse position: mouse + triangle-vertex + offset. var p_2d_px = this.mouse_2d_px.add(this.triangle_raw_2d_px[i]).add(offset_2d_px); // Put it in the triangle array. this.triangle_2d_px.push( p_2d_px); } drawPolygon( this.triangle_2d_px, {'borderColor':'white','borderWidth_px':1,'fillColor':this.color}); } Client.prototype.drawSelectionString = function() { this.selectionPoint_w_2d_px = screenFromWorld( this.selectionPoint_w_2d_m); // A nick name: var sP_2d_px = this.selectionPoint_w_2d_px; drawLine(sP_2d_px, this.mouse_2d_px, {'width_px':5,'color':'MediumSpringGreen'}); // Draw the small selection circle. drawCircle( sP_2d_px, {'borderColor':'white', 'borderWidth_px':2, 'fillColor':this.color, 'radius_px':6}); } function PuckTail( pars) { this.firstPoint_2d_m = pars.firstPoint_2d_m || new Vec2D( 1.0, 1.0); this.initial_radius_m = pars.initial_radius_m || 1.0; this.propSpeed_mps = pars.propSpeed_mps || 3.0; this.length_limit = pars.length_limit || 25; this.color = pars.color || 'lightgrey'; // The wait (time in seconds) before making a pure white color ping. this.markerPingTimerLimit_s = pars.markerPingTimerLimit_s || 1.0; this.markerPingTimer_s = 0.0; this.values = []; this.update( this.firstPoint_2d_m); } PuckTail.prototype.update = function( newPoint_2d_m) { if (this.markerPingTimer_s < this.markerPingTimerLimit_s) { this.pingColor = this.color; this.markerPingTimer_s += dt_frame_s; } else { this.pingColor = 'white'; this.markerPingTimer_s = 0; } // Ping out a new ring (once each frame). Each value is a position vector and radius. this.values.push({'p_2d_px':screenFromWorld( newPoint_2d_m), 'r_px':px_from_meters(this.initial_radius_m), 'color':this.pingColor}); // Remove the oldest value if needed. if (this.values.length > this.length_limit) { this.values.shift(); } // Loop through the tail. for (var t = 0, len = this.values.length; t < len; t++) { // Expand the radius of the ping (like a sound wave propagating). Note: doing this addition in pixels (not meters) // to yield a more consistent and pleasing rendering. this.values[t].r_px += px_from_meters( this.propSpeed_mps * c.deltaT_s); // Draw the sound circle. var lineColor = this.values[t].color; drawCircle( this.values[t].p_2d_px, {'radius_px':this.values[t].r_px, 'borderColor':lineColor, 'borderWidth_px':2, 'fillColor':'noFill'}); } } function Puck( position_2d_m, velocity_2d_mps, pars) { this.parsAtBirth = pars; this.bullet = pars.bullet || false; this.jello = pars.jello || false; this.clientName = pars.clientName || null; if (pars.name) { this.name = pars.name; Puck.nameIndex = Math.max(Puck.nameIndex, Number(this.name.slice(4, this.name.length))); } else { Puck.nameIndex += 1; this.name = 'puck' + Puck.nameIndex; } //console.log("n-puck = " + Puck.nameIndex); aT.puckMap[this.name] = this; // Position of Center of Mass (COM) this.position_2d_m = Vec2D_check( position_2d_m); // Position (in pixels). this.position_2d_px = screenFromWorld( this.position_2d_m); // Velocity of COM this.velocity_2d_mps = Vec2D_check( velocity_2d_mps); // Parse out the parameters in the pars object. The values on the right // are the defaults (used if pars value is undefined). this.color = pars.color || "DarkSlateGray"; this.shape = pars.shape || "circle"; this.colorSource = pars.colorSource || false; this.density = pars.density || 1.5; this.linearDamping = pars.linDamp || 0.0; this.hitLimit = pars.hitLimit || 10; this.createdByClient = pars.createdByClient || null; this.ageLimit_ms = pars.ageLimit_ms || null; this.tailSwitch = pars.tailSwitch || false; this.tail = null; this.groupIndex = pars.groupIndex || 0; this.categoryBits = pars.categoryBits || 0x0001; this.maskBits = pars.maskBits || 0xFFFF; // Rotational state this.angle_r = pars.angle_r || 0; this.angularSpeed_rps = pars.angularSpeed_rps || 0; this.borderWidth_px = pars.borderWidth_px || 3; // Put a reference to this puck in the client. if (this.clientName) { clients[this.clientName].puck = this; } this.createTime = window.performance.now(); // These two parameters are fixed (not affected by the g toggle) if they // are specified in the pars object. if (pars.restitution === undefined) { if (c.g_ON) { this.restitution = c.restitution_gOn; } else { this.restitution = c.restitution_gOff; } this.restitution_fixed = false; } else { this.restitution = pars.restitution; this.restitution_fixed = true; } if (pars.friction === undefined) { if (c.g_ON) { this.friction = c.friction_gOn; } else { this.friction = c.friction_gOff; } this.friction_fixed = false; } else { this.friction = pars.friction; this.friction_fixed = true; } // Dimensions this.radius_m = pars.radius_m || 1.0; this.aspectR = pars.aspectR || 1.0; this.half_height_m = pars.half_height_m || null; this.half_width_m = pars.half_width_m || null; if (this.shape == 'circle') { this.radius_px = px_from_meters( this.radius_m); // Rectangular } else { // Height and width given explicitly. if (this.half_height_m) { this.half_width_px = px_from_meters( this.half_width_m); this.half_height_px = px_from_meters( this.half_height_m); // Aspect ratio given. } else { this.half_width_m = this.radius_m * this.aspectR; this.half_width_px = px_from_meters( this.half_width_m); this.half_height_m = this.radius_m; this.half_height_px = px_from_meters( this.half_height_m); } } // Tail if (this.tailSwitch) { this.tail = new PuckTail({'firstPoint_2d_m':this.position_2d_m, 'initial_radius_m':this.radius_m}); } this.b2d = null; this.create_Box2d_Puck(); // Create a reference back to this puck from the b2d puck. // Note that a Map allows any type of object for the key! tableMap.set(this.b2d, this); this.cursorString_spring_force_2d_N = new Vec2D(0.0,0.0); this.cursorString_puckDrag_force_2d_N = new Vec2D(0.0,0.0); this.nonCOM_2d_N = []; this.sprDamp_force_2d_N = new Vec2D(0.0,0.0); this.jet_force_2d_N = new Vec2D(0.0,0.0); this.impulse_2d_Ns = new Vec2D(0.0,0.0); // Puck-popper features if (this.clientName) { // Add client controls and give each control a reference to this puck. this.jet = new Jet(this, {'initial_angle':10}); this.gun = new Gun(this, {'initial_angle':60, 'indicator':true, 'tube_color':'gray'}); } this.shield = new Shield(this, {'color':'yellow'}); this.hitCount = 0; this.poorHealthFraction = 0; this.flash = false; this.flashCount = 0; this.deleted = false; } Puck.nameIndex = 0; Puck.deleteAll = function() { for (var clientName in clients) { clients[ clientName].puck = null; } for (var puckName in aT.puckMap) { tableMap.delete( aT.puckMap[ puckName].b2d); world.DestroyBody( aT.puckMap[ puckName].b2d); } aT.jelloPucks = []; aT.puckMap = {}; Puck.nameIndex = 0; } Puck.prototype.deleteThisOne = function() { // JavaScript uses garbage collection. Deleting a puck involves // mainly nullifying all references to the puck. (Also removing references // from the puck.) this.deleted = true; this.jet = null; this.gun = null; this.shield = null; if (this.clientName == 'local') { // Must keep the local client. Just null out the puck reference in the local client. clients[this.clientName].puck = null; } else if (this.clientName) { // Remove the client on the node server. // (Note: this is the one place the hC is used inside of gW.) hC.forceClientDisconnect( this.clientName); // Remove the client in the clients map. delete clients[this.clientName]; } // Delete the corresponding Box2d object. tableMap.delete( this.b2d); world.DestroyBody( this.b2d); // Remove this puck from our puck map. delete aT.puckMap[ this.name]; delete aT.multiSelectMap[ this.name]; // Filter out this puck from the jelloPuck array. if (this.jello) { aT.jelloPucks = aT.jelloPucks.filter( function( eachPuck) { // Keep these (those NOT deleted) return (!eachPuck.deleted == true); }); } } Puck.prototype.copyThisOne = function() { // Make a copy of the mutable objects that are passed into the Puck constructor. var p_2d_m = Object.assign({}, this.position_2d_m); var v_2d_mps = Object.assign({}, this.velocity_2d_mps); var pars = Object.assign({}, this.parsAtBirth); // Make sure the name is nulled so the auto-naming feature is used in the constructor. pars.name = null; // Update pars to reflect any edits that have done. if (this.shape == 'circle'){ pars.radius_m = this.radius_m; } else { pars.half_height_m = this.half_height_m; pars.half_width_m = this.half_width_m; } new Puck( p_2d_m, v_2d_mps, pars); } Puck.prototype.updateState = function() { this.getPosition(); this.getVelocity(); this.getAngle(); this.getAngularSpeed(); } Puck.prototype.create_Box2d_Puck = function() { var bodyDef = new b2BodyDef; bodyDef.type = b2Body.b2_dynamicBody; // Make it be. this.b2d = world.CreateBody(bodyDef); this.b2d.CreateFixture( this.define_fixture( {}) ); // Set the state: position and velocity (angle and angular speed). this.b2d.SetPosition( this.position_2d_m); this.b2d.SetLinearVelocity( this.velocity_2d_mps); this.b2d.SetAngle( this.angle_r); this.b2d.SetAngularVelocity( this.angularSpeed_rps); // Use the mass calculated by box2d. this.mass_kg = this.b2d.GetMass(); //console.log("m=" + this.mass_kg); this.b2d.SetLinearDamping( this.linearDamping); this.b2d.SetBullet( this.bullet); } Puck.prototype.define_fixture = function( pars) { this.width_scaling = pars.width_scaling || 1.0; this.height_scaling = pars.height_scaling || 1.0; this.radius_scaling = pars.radius_scaling || 1.0; // Create a circular or rectangular dynamic box2d object. var fixDef = new b2FixtureDef; fixDef.density = this.density; fixDef.friction = this.friction; fixDef.restitution = this.restitution; fixDef.filter.groupIndex = this.groupIndex; fixDef.filter.categoryBits = this.categoryBits; fixDef.filter.maskBits = this.maskBits; if (this.shape == 'circle') { // Apply the radius scaling factor. this.radius_m *= this.radius_scaling; this.radius_px = px_from_meters( this.radius_m); // Don't let it get too small. if ((this.radius_px < 9) && (!this.bullet)) { this.radius_px = 9; this.radius_m = meters_from_px( this.radius_px); } // Don't let client pucks get so big that their bullets can collide with the body of their ship. if (this.clientName) { if (this.radius_m > this.parsAtBirth.radius_m) { this.radius_m = this.parsAtBirth.radius_m; this.radius_px = px_from_meters( this.radius_m); } } fixDef.shape = new b2CircleShape( this.radius_m); // Rectangular shapes } else { // Apply the scaling factors to the current width and height. this.half_width_m *= this.width_scaling; this.half_height_m *= this.height_scaling; this.half_width_px = px_from_meters( this.half_width_m); // Don't let it get too skinny because it becomes hard to select. if (this.half_width_px < 3) { this.half_width_px = 3; this.half_width_m = meters_from_px( this.half_width_px); } this.half_height_px = px_from_meters( this.half_height_m); if (this.half_height_px < 3) { this.half_height_px = 3; this.half_height_m = meters_from_px( this.half_height_px); } fixDef.shape = new b2PolygonShape; fixDef.shape.SetAsBox(this.half_width_m, this.half_height_m); } return fixDef; } Puck.prototype.modify_fixture = function( mode) { // For shape editing... // If you are going to modify the fixture dimensions you have to delete // the old one and make a new one. The m_fixtureList linked list always // points to the most recent addition to the linked list. If there's only // one fixture, then m_fixtureList is a reference to that single fixture. var width_factor = 1.0; var height_factor = 1.0; if (mode == 'wider') { width_factor = 1.1; } else if (mode == 'thinner') { width_factor = 0.9; } else if (mode == 'taller') { height_factor = 1.1; } else if (mode == 'shorter') { height_factor = 0.9; } this.b2d.DestroyFixture( this.b2d.m_fixtureList); if (this.shape == 'circle') { // Use either left/right or up/down to change the circle radius. if (width_factor == 1.0) width_factor = height_factor; this.b2d.CreateFixture( this.define_fixture({'radius_scaling':width_factor})); } else { this.b2d.CreateFixture( this.define_fixture({'width_scaling':width_factor,'height_scaling':height_factor})); } // Update the mass. this.mass_kg = this.b2d.GetMass(); // Update the puck tail if (this.tail){ this.tail.initial_radius_m = this.radius_m; } } Puck.prototype.getPosition = function() { this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition()); } Puck.prototype.getVelocity = function() { // COM velocity this.velocity_2d_mps = Vec2D_from_b2Vec2( this.b2d.GetLinearVelocity()); } Puck.prototype.getAngle = function() { // COM angle (radians) this.angle_r = this.b2d.GetAngle(); } Puck.prototype.getAngularSpeed = function() { // COM angular speed (radians per second) this.angularSpeed_rps = this.b2d.GetAngularVelocity(); } Puck.prototype.draw = function() { this.position_2d_px = screenFromWorld( this.position_2d_m); var borderColor; if (this.shape == 'circle') { // Draw the main circle. // If hit, color the border red for a few frames. if (this.flash) { borderColor = 'red'; this.flashCount += 1; if (this.flashCount >= 3) { this.flash = false; this.flashCount = 0; } } else { borderColor = 'white'; } drawCircle( this.position_2d_px, {'borderColor':borderColor, 'borderWidth_px':this.borderWidth_px, 'fillColor':this.color, 'radius_px':this.radius_px}); // Draw the health circle. this.poorHealthFraction = this.hitCount / this.hitLimit; var poorHealthRadius = this.radius_px * this.poorHealthFraction; if (poorHealthRadius > 0) { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'chocolate', 'radius_px':poorHealthRadius}); } // Update and draw the shield. if (clients[this.clientName]) this.shield.updateState(); // Show rotational orientation: draw a line segment along the line from the center out to a local point on the radius. var pointOnEdge_2d_px = screenFromWorld( this.b2d.GetWorldPoint( new b2Vec2(0.0, this.radius_m) ) ); var pointAtHalfRadius_2d_px = screenFromWorld( this.b2d.GetWorldPoint( new b2Vec2(0.0, this.radius_m * (1.0/2.0)) ) ); drawLine(pointAtHalfRadius_2d_px, pointOnEdge_2d_px, {'width_px':2,'color':'white'}); // Draw the tail if we have one. if (this.tailSwitch) this.tail.update( this.position_2d_m); } else { // Draw the rectangle. drawPolygon( b2d_getPolygonVertices( this.b2d), {'borderColor':'white','borderWidth_px':2,'fillColor':this.color}); } } Puck.prototype.draw_MultiSelectPoint = function() { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5}); } Puck.prototype.applyForces = function() { // Net resulting force on the puck. // First consider all forces acting on the COM. // F = acc * mass var puck_forces_2d_N = g_2d_mps2.scaleBy( this.mass_kg); puck_forces_2d_N.addTo( this.cursorString_spring_force_2d_N); puck_forces_2d_N.addTo( this.cursorString_puckDrag_force_2d_N); puck_forces_2d_N.addTo( this.sprDamp_force_2d_N); puck_forces_2d_N.addTo( this.jet_force_2d_N); puck_forces_2d_N.addTo( this.impulse_2d_Ns.scaleBy(1.0/c.deltaT_s)); // Apply this force to the puck's center of mass (COM) in the Box2d world this.b2d.ApplyForce( puck_forces_2d_N, this.position_2d_m); // Apply any non-COM forces in the array. for (var j = 0, len = this.nonCOM_2d_N.length; j < len; j++) { //console.log("force.force_2d_N.x = " + nonCOM_2d_N[j].force_2d_N.x); this.b2d.ApplyForce( this.nonCOM_2d_N[j].force_2d_N, this.nonCOM_2d_N[j].point_w_2d_m); } /* // Apply torques. #b2d //this.b2d.ApplyTorque( this.cursorString_torque_force_Nm, wake=True) */ // Now reset the aggregate forces. this.cursorString_spring_force_2d_N = new Vec2D(0.0,0.0); this.cursorString_puckDrag_force_2d_N = new Vec2D(0.0,0.0); this.nonCOM_2d_N = []; this.sprDamp_force_2d_N = new Vec2D(0.0,0.0); this.impulse_2d_Ns = new Vec2D(0.0,0.0); /* this.cursorString_torque_force_Nm = 0.0; */ } function Shield( puck, pars) { // Make a (circular) reference to the host puck. this.puck = puck; // Optional parameters and defaults. this.color = pars.color || 'lime'; // Make a direct reference to the client. this.client = clients[this.puck.clientName]; this.radius_px = px_from_meters( this.puck.radius_m * 1.15); this.ON = false; this.STRONG = true; this.STRONG_timer_s = 0; this.STRONG_time_limit_s = 3.0; this.CHARGING_timer_s = 0; this.CHARGING_time_limit_s = 2.0; this.charge_level = 1.0; } Shield.prototype.updateState = function() { // Let the client control the state and draw if ON. if (this.client.key_space == "D") { this.ON = true; if (this.STRONG) { var dashArray = [ 0]; } else { // Shields are weak. var dashArray = [10]; } drawCircle( this.puck.position_2d_px, {'borderColor':this.color, 'borderWidth_px':2, 'fillColor':'noFill', 'radius_px':this.radius_px, 'dashArray':dashArray}); } else { this.ON = false; } // Drain the shield if (this.ON && this.STRONG) { this.STRONG_timer_s += dt_frame_s; this.charge_level = 1.00 - (this.STRONG_timer_s / this.STRONG_time_limit_s); if (this.STRONG_timer_s > this.STRONG_time_limit_s) { this.STRONG = false; this.STRONG_timer_s = 0.0; } } // Recharge the shield only if completely drained. if (!this.STRONG) { this.CHARGING_timer_s += dt_frame_s; this.charge_level = this.CHARGING_timer_s / this.CHARGING_time_limit_s; if (this.CHARGING_timer_s > this.CHARGING_time_limit_s) { this.STRONG = true; this.CHARGING_timer_s = 0.0; } } // Display the shield timer on the gun tube. this.puck.gun.indicatorFraction = this.charge_level; } function Tube( puck, pars) { // Circular reference back to the puck. this.puck = puck; // Optional parameters and defaults. this.initial_angle = pars.initial_angle || 20; this.indicator = pars.indicator || false; // Make a direct reference to the client. this.client = clients[this.puck.clientName]; // 360 degrees/second / 60 frames/second = 6 degrees/frame this.rotationRate_dps = 240.0; //4.0dpf; this.tube_color = 'blue'; this.length_m = 1.05 * this.puck.radius_m; this.width_m = 0.30 * this.puck.radius_m; this.width_px = px_from_meters( this.width_m); // Establish the relative-position vector (for the end of the tube) using the length of the tube. this.rel_position_2d_m = new Vec2D(0.0, this.length_m); this.rel_position_2d_m.set_angle(this.initial_angle); this.AbsPositionOfEnds(); this.indicatorWidth_px = px_from_meters( this.width_m * 0.40); this.indicatorFraction = 0.00; //console.log('inside the Tube constructor.'); } Tube.prototype.AbsPositionOfEnds = function() { // Determine the absolute positions of the base and the end of the tube. this.base_2d_px = screenFromWorld( this.puck.position_2d_m); this.end_2d_m = this.puck.position_2d_m.add( this.rel_position_2d_m); this.end_2d_px = screenFromWorld( this.end_2d_m); } Tube.prototype.AbsPositionOfIndicator = function() { // The starting point will indicate the "amount" of the indicator. this.indicatorBase_2d_m = this.puck.position_2d_m.add( this.rel_position_2d_m.scaleBy(1 - this.indicatorFraction)); this.indicatorBase_2d_px = screenFromWorld( this.indicatorBase_2d_m); // Draw to the end of the tube. this.indicatorEnd_2d_m = this.puck.position_2d_m.add( this.rel_position_2d_m.scaleBy( 1.00)); this.indicatorEnd_2d_px = screenFromWorld( this.indicatorEnd_2d_m); } Tube.prototype.rotateTube = function( deg) { this.rel_position_2d_m.rotated_by( deg); } Tube.prototype.drawTube = function() { this.AbsPositionOfEnds(); drawLine(this.base_2d_px, this.end_2d_px, {'width_px':this.width_px,'color':this.tube_color}); if (this.indicator) { this.AbsPositionOfIndicator(); drawLine(this.indicatorBase_2d_px, this.indicatorEnd_2d_px, {'width_px':this.indicatorWidth_px, 'color':this.puck.shield.color}); } } function Jet( puck, pars) { // Call the Tube constructor. Tube.call(this, puck, pars); // Add properties specific to Jet. this.width_m = 0.17 * this.puck.radius_m; this.height_m = 1.00 * this.puck.radius_m; // This jet-flame triangle is oriented like an arrow pointing the positive x direction. this.triangle_2d_m = [new Vec2D(0,0), new Vec2D(0,-this.width_m), new Vec2D(this.height_m,0), new Vec2D(0,this.width_m)]; // Point the jet in the same direction as the tube. this.rotateJet( this.initial_angle); // Set the tube color to match the client color. this.tube_color = this.client.color; this.flame_color = 'red'; this.flameEdge_color = 'blue'; // Scaler magnitude this.jet_force_N = 1.3 * this.puck.mass_kg * Math.abs( g_mps2); } // Use the Tube prototype as starting point for the Jet (inheritance). This brings // in all the methods and attributes from Tube. Jet.prototype = Object.create( Tube.prototype, { // This object, passed as the second parameter (propertiesObject argument), is another way that you can // add in properties for Jet. 'example2': {value: 22, writable:true}, 'example3': {value:333, writable:true} }); // Set the constructor name to Jet, so it is not "Tube" (default). Jet.prototype.constructor = Jet; // Define any new methods for Jet. Jet.prototype.rotateJet = function( degrees) { for (var i = 0, len = this.triangle_2d_m.length; i < len; i++) { // Rotate each vertex. this.triangle_2d_m[i].rotated_by( degrees); } } Jet.prototype.rotateTubeAndJet = function( deg) { this.rotateTube( deg); this.rotateJet( deg); } Jet.prototype.rotateJetByClient = function() { // The Rate, degrees per frame (dpf), gives the degrees of rotation in one frame. // Left/Right pointing control if (this.client.key_d == "D") { this.rotateTubeAndJet(-this.rotationRate_dps * dt_frame_s); } if (this.client.key_a == "D") { this.rotateTubeAndJet(+this.rotationRate_dps * dt_frame_s); } // For use in stopping the puck... if ((this.client.key_s == "D") && (this.client.key_s_enabled)) { if (this.client.key_shift == "D") { // This simply flips the jet (180 degrees). this.rotateTubeAndJet(+180); } else { // This rotates the rel_position vector (the tube pointer) by the amount that it differs from the direction of motion. // The result being that it flips the tube to be in a direction opposite of the motion. this.rotateTubeAndJet(this.puck.velocity_2d_mps.get_angle() - this.rel_position_2d_m.get_angle()); } this.client.key_s_enabled = false; } if ((this.client.key_s == "U") && (!this.client.key_s_enabled)) { this.client.key_s_enabled = true; } } Jet.prototype.drawJetFlame = function() { // Before you can draw it, you have to know where it is on the screen. this.triangle_2d_px = []; for (var i = 0, len = this.triangle_2d_m.length; i < len; i++) { // Put it on the end of the tube. var p_2d_m = this.end_2d_m.add(this.triangle_2d_m[i]); var p_2d_px = screenFromWorld( p_2d_m); // Put it in the triangle array. this.triangle_2d_px.push( p_2d_px); } drawPolygon( this.triangle_2d_px, {'borderColor':this.flameEdge_color,'borderWidth_px':3,'fillColor':this.flame_color}); } Jet.prototype.updateAndDraw = function() { // Respond to client controls to rotate the Tube and Jet. this.rotateJetByClient(); // Always draw the tube. this.drawTube(); // If the jet is on (w key down), draw it, and calculate jet forces. if (this.client.key_w == "D") { this.drawJetFlame(); this.puck.jet_force_2d_N = this.rel_position_2d_m.scaleBy( -this.jet_force_N/this.length_m); } else { this.puck.jet_force_2d_N = this.rel_position_2d_m.scaleBy( 0); } } // (This ain't no BB gun). function Gun( puck, pars) { // Call the Tube constructor. Tube.call(this, puck, pars); this.tube_color = pars.tube_color || 'white'; // Add properties specific to Gun. this.width_m = 0.17 * this.puck.radius_m; this.height_m = 1.00 * this.puck.radius_m; this.rotationRate_dps = 90.0; //1.5dpf this.bulletSpeed_mps = 7.0; this.bulletCountLimit = 5; this.timeBetweenBullets_ms = 70; } // Use the Tube prototype as starting point for the Gun (inheritance). This brings // in all the methods and attributes from Tube. Gun.prototype = Object.create( Tube.prototype); // Set the constructor name to Gun, so it is not "Tube" (default). Gun.prototype.constructor = Gun; // Define any new methods for Gun. Gun.prototype.rotateGunByClient = function() { // The Rate, degrees per frame (dpf), gives the degrees of rotation in one frame. // Left/Right pointing control if (this.client.key_l == "D") { this.rotateTube(-this.rotationRate_dps * dt_frame_s); } if (this.client.key_j == "D") { this.rotateTube(+this.rotationRate_dps * dt_frame_s); } if ((this.client.key_k == "D") && (this.client.key_k_enabled)) { if (this.client.key_shift == "D") { this.rotateTube(+90.0); } else { this.rotateTube(-90.0); } this.client.key_k_enabled = false; } if ((this.client.key_k == "U") && (!this.client.key_k_enabled)) { this.client.key_k_enabled = true; } } Gun.prototype.fireBullet = function() { // The bullet velocity as seen from the puck. var relativeVel_2D_mps = this.rel_position_2d_m.scaleBy( this.bulletSpeed_mps/this.length_m); // Absolute velocity of bullet as seen from the world. var absoluteVel_2D_mps = relativeVel_2D_mps.add( this.puck.velocity_2d_mps); var bullet = new Puck( this.end_2d_m, absoluteVel_2D_mps, {'radius_m':0.04, 'bullet':true, 'color':this.client.color, 'borderWidth_px':1, 'createdByClient':this.client.name, 'ageLimit_ms':1500, 'restitution_fixed':true , 'restitution':1.0}); // Calculate the recoil impulse from firing the gun (opposite the direction of the bullet). this.puck.impulse_2d_Ns.addTo( relativeVel_2D_mps.scaleBy(-1 * bullet.mass_kg)); } Gun.prototype.start_BulletStream = function() { this.bulletCount = 1; this.bulletStream = 'on'; // This allows the gun to immediately fire the first bullet. this.timeLastFired = window.performance.now() - this.timeBetweenBullets_ms; } Gun.prototype.stop_BulletStream = function() { this.bulletStream = 'off'; } Gun.prototype.update_BulletStream = function() { var deltaTime_ms = window.performance.now() - this.timeLastFired; // If ok to fire, do so. if ((this.bulletStream == 'on') && (deltaTime_ms >= this.timeBetweenBullets_ms) && (this.bulletCount <= this.bulletCountLimit)) { // If the shields are down. if (!this.puck.shield.ON) { this.fireBullet(); } this.timeLastFired = window.performance.now(); this.bulletCount += 1; } } Gun.prototype.updateAndDraw = function() { // Respond to client controls to rotate the Gun. this.rotateGunByClient(); // Always draw the tube. this.drawTube(); // Fire the gun: // This draw method gets called every frame. If the i key is down, you // don't want it to fire a bullet every frame. The following logic allows one // call to fireBullet and then disables the i key. To enable, must release // the key to the up position. if (this.client.key_i == "D") { if (this.client.key_i_enabled) { this.start_BulletStream(); this.client.key_i_enabled = false; //console.log("gun draw: i down and disabled."); } this.update_BulletStream(); } else if ((this.client.key_i == "U") && (!this.client.key_i_enabled)) { this.stop_BulletStream(); this.client.key_i_enabled = true; //console.log("gun draw: i up and enabled."); } } function Pin( position_2d_m, pars) { this.parsAtBirth = pars; if (pars.name) { this.name = pars.name; Pin.nameIndex = Math.max(Pin.nameIndex, Number(this.name.slice(3, this.name.length))); } else { Pin.nameIndex += 1; this.name = 'pin' + Pin.nameIndex; } //console.log("n-pin = " + Pin.nameIndex); aT.pinMap[this.name] = this; this.position_2d_m = Vec2D_check( position_2d_m); this.position_2d_px = screenFromWorld( this.position_2d_m); this.radius_px = pars.radius_px || 6; // Make the radius in box2d a little larger so can select it easier. this.radius_m = meters_from_px( this.radius_px + 2); // Masking parameters for b2d object (default is to prevent collisions with the pin) //this.groupIndex = pars.groupIndex || 0; this.categoryBits = pars.categoryBits || 0x0000; this.maskBits = pars.maskBits || 0x0000; this.velocity_2d_mps = new Vec2D(0.0,0.0); this.b2d = null; this.create_b2d_pin(); // Create a reference back to this pin from the b2d pin. tableMap.set(this.b2d, this); this.deleted = false; /* console.log('inside pin constructor'); for (var pinMapKey in aT.pinMap) { console.log("key in pinMap = " + pinMapKey); } */ } Pin.nameIndex = 0; Pin.deleteAll = function () { for (var pinName in aT.pinMap) { tableMap.delete( aT.pinMap[ pinName].b2d); world.DestroyBody( aT.pinMap[ pinName].b2d); } aT.pinMap = {}; Pin.nameIndex = 0; } Pin.prototype.deleteThisOne = function() { // Delete reference in the tableMap. tableMap.delete( this.b2d); // Delete the corresponding Box2d object. world.DestroyBody( this.b2d); // Mark this pin as deleted. this.deleted = true; // Remove this pin from the pin map. delete aT.pinMap[ this.name]; } Pin.prototype.copyThisOne = function() { var p_2d_m = Object.assign({}, this.position_2d_m); var pars = Object.assign({}, this.parsAtBirth); // Make sure the name is nulled so the auto-naming feature is used in the constructor. pars.name = null; new Pin( p_2d_m, pars); } Pin.prototype.define_fixture = function() { var fixDef = new b2FixtureDef; fixDef.filter.groupIndex = this.groupIndex; fixDef.filter.categoryBits = this.categoryBits; fixDef.filter.maskBits = this.maskBits; fixDef.shape = new b2CircleShape( this.radius_m); return fixDef; } Pin.prototype.create_b2d_pin = function() { // Create a rectangular and static box2d object. var bodyDef = new b2BodyDef; bodyDef.type = b2Body.b2_kinematicBody; // b2_kinematicBody b2_staticBody this.b2d = world.CreateBody(bodyDef); this.b2d.CreateFixture( this.define_fixture()); // Set the state: position and velocity (angle and angular speed). this.b2d.SetPosition( this.position_2d_m); } Pin.prototype.getPosition = function() { this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition()); this.position_2d_px = screenFromWorld( this.position_2d_m); } Pin.prototype.draw_MultiSelectPoint = function() { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5}); } Pin.prototype.draw = function() { this.getPosition(); drawCircle( this.position_2d_px, {'borderColor':'yellow', 'borderWidth_px':2, 'fillColor':'blue', 'radius_px':this.radius_px}); } function Spring(puck1, puckOrPin2, pars) { this.parsAtBirth = pars; if (pars.name) { this.name = pars.name; Spring.nameIndex = Math.max(Spring.nameIndex, Number(this.name.slice(1, this.name.length))); } else { Spring.nameIndex += 1; this.name = 's' + Spring.nameIndex; } //console.log("n-spring = " + Spring.nameIndex); aT.springMap[this.name] = this; this.color = pars.color || "red"; this.length_m = pars.length_m || 3.0; this.strength_Npm = pars.strength_Npm || 0.5; this.unstretched_width_m = pars.unstretched_width_m || 0.025; this.drag_c = pars.drag_c || 0.0; this.damper_Ns2pm2 = pars.damper_Ns2pm2 || 0.5; this.dashedLine = pars.dashedLine || false; // Spring Puck Object (spo1, not p1). Giving this a distinctive name so that it can be filtered // out in the JSON capture. This filtering avoids some wordiness in the capture. this.spo1 = puck1; this.p1_name = puck1.name; // Same reasoning here for the distinctive name (spo2, not p2). this.spo2 = puckOrPin2; this.p2_name = puckOrPin2.name; // Pin one end of the spring to a fixed location. if (this.spo2.constructor.name == "Pin") { this.length_m = 0.0; this.pinned = true; } else { this.pinned = false; } this.p1p2_separation_2d_m = new Vec2D(0,0); this.p1p2_separation_m = 0; this.p1p2_normalized_2d = new Vec2D(0,0); } Spring.nameIndex = 0; Spring.deleteAll = function () { /* for (var springName in aT.springMap) { tableMap.delete( aT.springMap[ springName].b2d); world.DestroyBody( aT.springMap[ springName].b2d); } */ aT.springMap = {}; Spring.nameIndex = 0; } Spring.findInMultiSelect = function ( doThis) { // Find all the springs that have both ends (puck or pin) in the multi-select map. for (var springName in aT.springMap) { if ((aT.springMap[ springName].spo1.name in aT.multiSelectMap) && (aT.springMap[ springName].spo2.name in aT.multiSelectMap)) { // For each spring you find. doThis( springName); } } } Spring.prototype.deleteThisOne = function() { // Remove this spring from the spring map. delete aT.springMap[ this.name]; } Spring.prototype.copyThisOne = function(p1, p2) { // Make a copy of the mutable objects that are passed into the Spring constructor. var pars = Object.assign({}, this.parsAtBirth); // Null the name so the auto-naming feature is used in the constructor. pars.name = null; pars.length_m = this.length_m; pars.unstretched_width_m = this.unstretched_width_m pars.strength_Npm = this.strength_Npm; // Hang this spring on p1 and p2 (don't allow the case where both are pins). if (!((p1.constructor.name == "Pin") && (p2.constructor.name == "Pin"))) { if (p1.constructor.name == "Pin") { new Spring( p2, p1, pars); } else { // This one includes the cases for no-pins and p2 pinned. new Spring( p1, p2, pars); } } } Spring.prototype.modify_fixture = function( mode) { var width_factor = 1.0; var length_factor = 1.0; if (mode == 'wider') { width_factor = 1.1; } else if (mode == 'thinner') { width_factor = 0.9; } else if (mode == 'taller') { length_factor = 1.1; } else if (mode == 'shorter') { length_factor = 0.9; } this.length_m *= length_factor; // Use the wider/thinner width_factor to affect both the visual width and strength of the spring. this.unstretched_width_m *= width_factor; this.strength_Npm *= width_factor; } Spring.prototype.force_on_pucks = function() { this.p1p2_separation_2d_m = this.spo1.position_2d_m.subtract( this.spo2.position_2d_m); this.p1p2_separation_m = this.p1p2_separation_2d_m.length(); // The pinned case needs to be able to handle the zero length spring. The // separation distance will be zero when the pinned spring is at rest. // This will cause a divide by zero error if not handled here. if ((this.p1p2_separation_m == 0.0) && (this.length_m == 0.0)) { var spring_force_on_1_2d_N = new Vec2D(0.0,0.0); } else { this.p1p2_normalized_2d = this.p1p2_separation_2d_m.scaleBy( 1/this.p1p2_separation_m); // Spring force: acts along the separation vector and is proportional to the separation distance. var spring_force_on_1_2d_N = this.p1p2_normalized_2d.scaleBy( (this.length_m - this.p1p2_separation_m) * this.strength_Npm); } // Damper force: acts along the separation vector and is proportional to the relative speed. var v_relative_2d_mps = this.spo1.velocity_2d_mps.subtract( this.spo2.velocity_2d_mps); var v_relative_alongNormal_2d_mps = v_relative_2d_mps.projection_onto( this.p1p2_separation_2d_m); if (v_relative_alongNormal_2d_mps == null) {v_relative_alongNormal_2d_mps = v_relative_2d_mps.scaleBy(0.0)} var damper_force_on_1_2d_N = v_relative_alongNormal_2d_mps.scaleBy( this.damper_Ns2pm2); // Net force by both spring and damper var sprDamp_force_2d_N = spring_force_on_1_2d_N.subtract( damper_force_on_1_2d_N); // This force acts in opposite directions for each of the two pucks. Notice the "addTo" here, this // is an aggregate across all the springs. This aggregate MUST be reset (zeroed) after the movements are // calculated. So by the time you've looped through all the springs, you get the NET force, one each ball, // applied of all individual springs. this.spo1.sprDamp_force_2d_N.addTo( sprDamp_force_2d_N.scaleBy( +1)); if (this.spo2.constructor.name != "Pin") { this.spo2.sprDamp_force_2d_N.addTo( sprDamp_force_2d_N.scaleBy( -1)); } // Add in some drag forces if a non-zero drag coefficient is specified. These are based on the // velocity of the pucks (not relative speed as is the case above for damper forces). this.spo1.sprDamp_force_2d_N.addTo( this.spo1.velocity_2d_mps.scaleBy( -1 * this.drag_c)); if (this.spo2.constructor.name != "Pin") { this.spo2.sprDamp_force_2d_N.addTo( this.spo2.velocity_2d_mps.scaleBy( -1 * this.drag_c)); } } Spring.prototype.draw = function() { var width_m = this.unstretched_width_m * (1 + 0.30 * (this.length_m - this.p1p2_separation_m)); var width_px = px_from_meters( width_m); if (width_px < 2) {width_px = 2}; if (this.dashedLine) { var dashArray = [3]; } else { var dashArray = [0]; } drawLine( this.spo1.position_2d_px, this.spo2.position_2d_px, {'width_px':width_px, 'color':this.color, 'dashArray':dashArray} ); //if (this.spo2.constructor.name == "Pin") { // this.spo2.draw(); //} } function Wall( position_2d_m, pars) { this.parsAtBirth = pars; if (pars.name) { this.name = pars.name; // Set nameIndex to the max of the two indexes. Do this to avoid issues related to holes // in the name sequence caused by state captures after object deletions. This insures a // unique new name for any new wall. Wall.nameIndex = Math.max(Wall.nameIndex, Number(this.name.slice(4, this.name.length))); } else { Wall.nameIndex += 1; this.name = 'wall' + Wall.nameIndex; } //console.log("n-wall = " + Wall.nameIndex); aT.wallMap[this.name] = this; // Position of Center of Mass (COM) this.position_2d_m = Vec2D_check( position_2d_m); this.position_2d_px = screenFromWorld( this.position_2d_m); this.fence = pars.fence || false; this.velocity_2d_mps = pars.velocity_2d_mps || new Vec2D(0.0, 0.0); this.angle_r = pars.angle_r || 0.0; this.angularSpeed_rps = pars.angularSpeed_rps || 0.0; // Dimensions (as specified in box2D) this.half_width_m = pars.half_width_m || 0.5; this.half_height_m = pars.half_height_m || 2.5; // Calculate these characteristics in screen units (pixels). this.half_width_px = px_from_meters( this.half_width_m); this.half_height_px = px_from_meters( this.half_height_m); this.b2d = null; this.create_b2d_wall(); // Create a reference back to this wall from the b2d wall. tableMap.set(this.b2d, this); var color_default; Wall.color_default = "white"; this.color = Wall.color_default; this.deleted = false; } Wall.nameIndex = 0; Wall.deleteAll = function() { for (var wallName in aT.wallMap) { tableMap.delete( aT.wallMap[ wallName].b2d); world.DestroyBody( aT.wallMap[ wallName].b2d); } aT.wallMap = {}; Wall.nameIndex = 0; } Wall.prototype.deleteThisOne = function() { // Delete reference in the tableMap. tableMap.delete( this.b2d); // Delete the corresponding Box2d object. world.DestroyBody( this.b2d); // Mark this wall as deleted. this.deleted = true; // Remove this wall from the wall map. delete aT.wallMap[ this.name]; } Wall.prototype.copyThisOne = function() { new Wall( this.position_2d_m, {'half_width_m':this.half_width_m, 'half_height_m':this.half_height_m, 'angle_r':this.angle_r, 'angularSpeed_rps':this.angularSpeed_rps}); } Wall.prototype.define_fixture = function( pars) { this.width_scaling = pars.width_scaling || 1.0; this.height_scaling = pars.height_scaling || 1.0; var fixDef = new b2FixtureDef; fixDef.shape = new b2PolygonShape; // Apply the scaling factors to the current width and height. this.half_width_m *= this.width_scaling; this.half_height_m *= this.height_scaling; this.half_width_px = px_from_meters( this.half_width_m); // Don't let it get too skinny because it becomes hard to select. if (this.half_width_px < 1) { this.half_width_px = 1; this.half_width_m = meters_from_px( this.half_width_px); } this.half_height_px = px_from_meters( this.half_height_m); if (this.half_height_px < 1) { this.half_height_px = 1; this.half_height_m = meters_from_px( this.half_height_px); } fixDef.shape.SetAsBox(this.half_width_m, this.half_height_m); //console.log('hh_m=' + this.half_height_m); return fixDef; } Wall.prototype.create_b2d_wall = function() { // Create a rectangular and static box2d object. var bodyDef = new b2BodyDef; bodyDef.type = b2Body.b2_kinematicBody; // b2_kinematicBody b2_staticBody this.b2d = world.CreateBody(bodyDef); this.b2d.CreateFixture( this.define_fixture({})); // Set the state: position and velocity (angle and angular speed). this.b2d.SetPosition( this.position_2d_m); this.b2d.SetLinearVelocity( this.velocity_2d_mps); this.b2d.SetAngle( this.angle_r); this.b2d.SetAngularVelocity( this.angularSpeed_rps); } Wall.prototype.modify_fixture = function( mode) { // If you are going to modify the fixture dimensions you have to delete // the old one and make a new one. The m_fixtureList linked list always // points to the most recent addition to the linked list. If there's only // one fixture, then m_fixtureList is a reference to that single fixture. var width_factor = 1.0; var height_factor = 1.0; if (mode == 'wider') { width_factor = 1.1; } else if (mode == 'thinner') { width_factor = 0.9; } else if (mode == 'taller') { height_factor = 1.1; } else if (mode == 'shorter') { height_factor = 0.9; } this.b2d.DestroyFixture( this.b2d.m_fixtureList); this.b2d.CreateFixture( this.define_fixture({'width_scaling':width_factor,'height_scaling':height_factor})); } Wall.prototype.draw_MultiSelectPoint = function() { drawCircle( this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5}); } Wall.prototype.draw = function() { /* ctx.fillStyle = this.color; // Canvas draws a rectangle based on an upper-left-corner position. ctx.fillRect(this.position_2d_px.x - this.half_width_px, this.position_2d_px.y - this.half_height_px, this.half_width_px * 2, this.half_height_px * 2); */ drawPolygon( b2d_getPolygonVertices( this.b2d), {'borderColor':this.color, 'borderWidth_px':0, 'fillColor':this.color}); } function RunningAverage( n_target) { this.n_target = n_target; this.reset(); } RunningAverage.prototype.reset = function() { this.n_in_avg = 0; this.result = 0.0; this.values = []; this.total = 0.0; this.totalSinceReport = 0.0; } RunningAverage.prototype.update = function( new_value) { if (this.n_in_avg < this.n_target) { this.total += new_value; this.n_in_avg += 1; } else { // Add the new value and subtract the oldest. this.total += new_value - this.values[0]; // Discard the oldest value. this.values.shift(); } this.values.push(new_value); this.totalSinceReport += new_value; this.result = this.total / this.n_in_avg; return this.result; } ///////////////////////////////////////////////////////////////////////////// //// //// Functions //// ///////////////////////////////////////////////////////////////////////////// // Support for the network client /////////////////////////////////////////// function createNetworkClient( clientName) { var n = clientName.slice(1); var colorIndex = n - Math.trunc(n/10)*10; var pars = {}; pars.color = Client.colors[ colorIndex]; pars.name = clientName; clients[ clientName] = new Client( pars); } function deleteNetworkClient( clientName) { // This function does not directly remove the client socket at the node server, but // that does happen at the server... if (clients[clientName]) { if (clients[clientName].puck) { var thePuck = clients[clientName].puck // Remove this puck and do associated clean-up. thePuck.jet = null; thePuck.gun = null; thePuck.shield = null; tableMap.delete( thePuck.b2d); world.DestroyBody( thePuck.b2d); delete aT.puckMap[ thePuck.name]; } delete clients[ clientName]; } } function updateClientState( clientName, state) { // This is mouse and keyboard input as generated from non-host-client events. // Note that this can happen at anytime as triggered by events of the client. if (clients[ clientName]) { clients[ clientName].mouseX_px = state.mX; clients[ clientName].mouseY_px = state.mY; clients[ clientName].isMouseDown = state.MD; // Immediately unselect the puck of the network client if the mouse is up. if (clients[ clientName].isMouseDown == false) clients[ clientName].selectedBody = null; //clients[ clientName].checkForMouseSelection(); clients[ clientName].button = state.bu; clients[ clientName].mouse_2d_px = new Vec2D(clients[ clientName].mouseX_px, clients[ clientName].mouseY_px); clients[ clientName].mouse_2d_m = worldFromScreen( clients[ clientName].mouse_2d_px); clients[ clientName].key_a = state.a; clients[ clientName].key_s = state.s; clients[ clientName].key_d = state.d; clients[ clientName].key_w = state.w; clients[ clientName].key_j = state.j; clients[ clientName].key_k = state.k; clients[ clientName].key_l = state.l; clients[ clientName].key_i = state.i; clients[ clientName].key_space = state.sp; clients[ clientName].key_f = state.f; if ((state.f == "D") && (clients[ clientName].key_f_enabled)) { freeze(); // inhibit the freeze until the f key goes up again. clients[ clientName].key_f_enabled = false; } if ((state.f == "U") && (!clients[ clientName].key_f_enabled)) { clients[ clientName].key_f_enabled = true; } /* var stateString = ""; for (var key in state) stateString += key + ":" + state[key] + ","; console.log(stateString); */ } } // box2d functions to interact with the engine ////////////////////////////// function b2d_getBodyAt( mousePVec_2d_m) { var x = mousePVec_2d_m.x; var y = mousePVec_2d_m.y; var aabb = new b2AABB(); aabb.lowerBound.Set(x - 0.001, y - 0.001); aabb.upperBound.Set(x + 0.001, y + 0.001); // Query the world for overlapping shapes. var selectedBody = null; world.QueryAABB( function( fixture) { if(fixture.GetBody().GetType() != b2Body.b2_staticBody) { // b2_kinematicBody if(fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), mousePVec_2d_m)) { selectedBody = fixture.GetBody(); return false; } } return true; }, aabb); return selectedBody; } function b2d_getPolygonVertices( b2d_body) { // Make an array that has the world vertices scaled to screen coordinates. var poly_px = []; for (var i = 0; i < b2d_body.m_fixtureList.m_shape.m_vertices.length; i++) { var p_2d_px = screenFromWorld( b2d_body.GetWorldPoint( b2d_body.m_fixtureList.m_shape.m_vertices[i])); poly_px.push( p_2d_px); } return poly_px; } // Relationships between the screen and the b2d world /////////////////////// // Scaler conversions function meters_from_px( length_px) { return length_px / px_per_m; } function px_from_meters( length_m) { return Math.round(length_m * px_per_m); } // Vector conversions. function screenFromWorld( position_2d_m) { var x_px = px_from_meters( position_2d_m.x); var y_px = px_from_meters( position_2d_m.y); return new Vec2D( x_px, canvas.height - y_px); } function worldFromScreen( position_2d_px) { var x_m = meters_from_px( position_2d_px.x); var y_m = meters_from_px( canvas.height - position_2d_px.y); return new Vec2D( x_m, y_m); } // Functions to convert vector types function Vec2D_from_b2Vec2( b2Vector) { return new Vec2D( b2Vector.x, b2Vector.y); } // This check is useful to prevent problems (objects stripped of their methods) when reconstructing from a // JSON capture. function Vec2D_check( vector_2d) { if (vector_2d.constructor.name == "Vec2D") { return vector_2d; } else { return new Vec2D( vector_2d.x, vector_2d.y); } } // High-level functions for drawing to the Canvas function drawLine(p1_2d_px, p2_2d_px, pars) { ctx.strokeStyle = pars.color || 'white'; ctx.lineWidth = pars.width_px || 2; var dashArray = pars.dashArray || [0]; ctx.setLineDash( dashArray); ctx.beginPath(); ctx.moveTo(p1_2d_px.x, p1_2d_px.y); ctx.lineTo(p2_2d_px.x, p2_2d_px.y); ctx.stroke(); } function drawCircle( center_2d_px, pars) { var radius_px = pars.radius_px || 6; ctx.strokeStyle = pars.borderColor || 'white'; ctx.lineWidth = pars.borderWidth_px || 2; var fillColor = pars.fillColor || 'red'; var dashArray = pars.dashArray || [0]; ctx.setLineDash( dashArray); ctx.beginPath(); ctx.arc(center_2d_px.x, center_2d_px.y, radius_px, 0, 2 * Math.PI); if (fillColor != 'noFill') { ctx.fillStyle = fillColor; ctx.fill(); } ctx.stroke(); // Turn off the dashes. Remember, ctx is global... ctx.setLineDash([0]); } function drawPolygon( poly_px, pars) { ctx.strokeStyle = pars.borderColor || 'white'; ctx.lineWidth = pars.borderWidth_px || 2; ctx.fillStyle = pars.fillColor || 'red'; ctx.beginPath(); ctx.moveTo(poly_px[0].x, poly_px[0].y); for (var i = 1, len = poly_px.length; i < len; i++) { ctx.lineTo(poly_px[i].x, poly_px[i].y); } //ctx.lineTo(poly_px[0].x, poly_px[0].y); ctx.closePath(); ctx.fill(); ctx.stroke(); } // Functions called by the buttons ////////////////////////////////////////// function toggleElementDisplay(id, displayStyle) { var e = document.getElementById(id); // Use ternary operator (?): condition ? expr1 : expr2 // If the current style isn't equal to displayStyle, set it to be displayStyle. If it is equal, set it to 'none'. // When the value is 'none', the element is hidden. e.style.display = (e.style.display != displayStyle) ? displayStyle : 'none'; } function toggleSpanValue(id, value1, value2) { var e = document.getElementById(id); e.innerText = (e.innerText == value1) ? value2 : value1; } function resetFenceColor( newColor) { for (var wallName in aT.wallMap) { var theWall = aT.wallMap[ wallName]; if (theWall.fence) { theWall.color = newColor; theWall.draw(); } } } function startit() { // Only start a game loop if there is no game loop running. if (myRequest === null) { resetFenceColor( "white"); dC.pause.checked = false; // Start the game loop. myRequest = window.requestAnimationFrame( gameLoop); } } function stopit() { resetFenceColor( "red"); aT.dt_RA_ms.reset(); dC.fps.innerHTML = '0'; window.cancelAnimationFrame( myRequest); myRequest = null; resumingAfterPause = true; } function setFrameRate() { var frameRate = $('#FrameRate').val(); console.log("Frame Rate = " + frameRate); if (frameRate != 'float') { c.deltaT_s = 1.0 / frameRate; c.dtFloating = false; } else { c.dtFloating = true; } } function freeze() { for (var puckName in aT.puckMap) { aT.puckMap[ puckName].b2d.SetLinearVelocity( new b2Vec2(0.0,0.0)); } } function stopRotation() { for (var puckName in aT.puckMap) { aT.puckMap[ puckName].b2d.SetAngularVelocity( 0.0); } } function saveState() { var tableState = {'demoIndex':c.demoIndex, 'gravity':c.g_ON, 'wallMapData':aT.wallMap, 'puckMapData':aT.puckMap, 'pinMapData':aT.pinMap, 'springMapData':aT.springMap}; // Use this function to exclude the b2d objects in the stringify process. Apparently // the b2d objects have circular references that stringify doesn't like. So have // to regenerate the b2d objects in the demo area when the json capture is restored. // Also have to avoid the client related addons: jet, gun, and shield. These have references // back their pucks, this too causes circular issue for stringify. // Remove the spo1 and spo2 keys (from Springs object) mainly to keep the wordiness // down; don't need them in the reconstruction process. // || (key == 'spo1') || (key == 'spo2') // So be careful here: any key with a name in the OR list below will be excluded. var table_JSON = JSON.stringify( tableState, function(key, value) { if ((key == 'b2d') || (key == 'jet') || (key == 'gun') || (key == 'shield') || (key == 'spo1') || (key == 'spo2')) { return undefined; } else { return value; } }); // Write the json string to this visible input field. dC.json.value = table_JSON; // Select, copy to clipboard, and then remove focus from the input field. dC.json.select(); document.execCommand('copy'); window.getSelection().removeAllRanges(); // this is necessary for the blur method to work in MS Edge. dC.json.blur(); // jquery test for isolating the Edge issue above. //$( "#chkEditor" ).focus(); //$( "#chkEditor" ).blur(); } function clearState() { dC.json.value = ''; } function newBirth( captureObj) { // Update the birth object (based on the capture state) and use it for restoration. var newBirthState = {}; for (var birthParm in captureObj.parsAtBirth) { newBirthState[ birthParm] = captureObj[ birthParm]; } // To override the default naming process, specify a name in the birth parameters. This gives // the new object the name used in the capture object. This is critical in reconstructing // springs (that use the original puck name). This is all especially important if pucks are // deleted in a jello matrix. if (captureObj.name) { newBirthState.name = captureObj.name; } return newBirthState; } function restoreFromState( state_data) { // Environmental parameters... c.g_ON = state_data.gravity; dC.gravity.checked = c.g_ON; setGravityRelatedParameters(); // Rebuild the walls from the capture data. for (var wallName in state_data.wallMapData) { // wall is one wall (captured state) var wall = state_data.wallMapData[ wallName]; // Add some rotation-related keys to the capture object's parsAtBirth. if (wall.angle_r) wall.parsAtBirth.angle_r = null; if (wall.angularSpeed_rps) wall.parsAtBirth.angularSpeed_rps = null; // Create the new Wall and add it to the wallMap (via its constructor). new Wall( wall.position_2d_m, newBirth( wall)); } // Rebuild the pucks (and the puck map). for (var p_key in state_data.puckMapData) { // puck is a single puck (captured state) var puck = state_data.puckMapData[ p_key]; // Add some rotation-related keys to the capture object's parsAtBirth. if (puck.angle_r) puck.parsAtBirth.angle_r = null; if (puck.angularSpeed_rps) puck.parsAtBirth.angularSpeed_rps = null; // Now create the puck and give it the old name. // Note that vectors are recreated here because the state capture doesn't include any methods (functions). if ((!puck.bullet) && (puck.clientName == null)) { var newPuck = new Puck( puck.position_2d_m, puck.velocity_2d_mps, newBirth( puck)); if (puck.jello) { aT.jelloPucks.push( newPuck); } } } // Rebuild the pins. for (var pinName in state_data.pinMapData) { // pin is one pin (captured state) var pin = state_data.pinMapData[ pinName]; // Create the new Pin and add it to the pinMap (via its constructor). new Pin( pin.position_2d_m, newBirth( pin)); } // Rebuild the spring. for (var springName in state_data.springMapData) { var theSpring = state_data.springMapData[ springName]; var p1 = aT.puckMap[ theSpring.p1_name]; if (theSpring.pinned) { var p2 = aT.pinMap[ theSpring.p2_name]; } else { var p2 = aT.puckMap[ theSpring.p2_name]; } new Spring(p1, p2, newBirth( theSpring)); } } // Functions in support of the demos //////////////////////////////////////// function scrollDemoHelp( targetID){ var container = $('#helpScroller'); var scrollTo = $(targetID); var tweak_px = -6; /* console.log('--------'); console.log('scrollTo.offset().top: ' + scrollTo.offset().top); console.log('container.offset().top: ' + container.offset().top); console.log('container.scrollTop(): ' + container.scrollTop()); console.log('tweak_px: ' + tweak_px); */ container.animate( {scrollTop: scrollTo.offset().top - container.offset().top + container.scrollTop() + tweak_px}, 500 ); } function toggleMultiplayerStuff() { //This has the effect of switching between the following two divs. toggleElementDisplay("multiPlayer", "table-cell"); toggleElementDisplay("ttcIntro", "table-cell"); toggleElementDisplay("clientLinks", "inline"); } // Editor help toggle function openDemoHelp() { // Not using this anymore. A bit confusing. Might bring it back. if (dC.multiplayer.checked) { dC.multiplayer.checked = !dC.multiplayer.checked; toggleMultiplayerStuff(); } toggleElementDisplay('outline1','block'); toggleSpanValue('moreOrLess','More','Less'); toggleSpanValue('moreOrLess2','More','Less'); scrollDemoHelp('#editorMark'); } function resetFrictionParameters() { c.restitution_gOn = c.restitution_default_gOn; c.friction_gOn = c.friction_default_gOn; c.restitution_gOff = c.restitution_default_gOff; c.friction_gOff = c.friction_default_gOff; } function setGravityRelatedParameters() { if (c.g_ON) { Box2D.Common.b2Settings.b2_velocityThreshold = 1.0; g_2d_mps2 = new Vec2D(0.0, -g_mps2); // Global var restitution = c.restitution_gOn; var friction = c.friction_gOn; } else { Box2D.Common.b2Settings.b2_velocityThreshold = 0.0; g_2d_mps2 = new Vec2D(0.0, 0.0); // Global var restitution = c.restitution_gOff; var friction = c.friction_gOff; } // If there are some existing pucks on the table: // If not fixed, set restitution and friction properties. for (var puckName in aT.puckMap) { if (!aT.puckMap[ puckName].restitution_fixed) { aT.puckMap[ puckName].b2d.m_fixtureList.m_restitution = restitution; } if (!aT.puckMap[ puckName].friction_fixed) { aT.puckMap[ puckName].b2d.m_fixtureList.m_friction = friction; } } } function make_fence() { // Build perimeter fence (4 walls) using the canvas dimensions. var width_m = meters_from_px( canvas.width ); var half_width_m = width_m / 2.0; var height_m = meters_from_px( canvas.height); var half_height_m = height_m / 2.0; var wall_thickness_m = 0.10; var pull_in_m = 0.0; var short_wide_dimensions = {'fence':true, 'half_width_m':half_width_m, 'half_height_m':wall_thickness_m/2.0}; var tall_skinny_dimensions = {'fence':true, 'half_width_m':wall_thickness_m/2.0, 'half_height_m':half_height_m}; // Add four bumper walls to the table. // top new Wall( new Vec2D( half_width_m, height_m - pull_in_m), short_wide_dimensions); // bottom new Wall( new Vec2D( half_width_m, 0.00 + pull_in_m), short_wide_dimensions); // left new Wall( new Vec2D( 0.00 + pull_in_m, half_height_m), tall_skinny_dimensions); // right new Wall( new Vec2D( width_m - pull_in_m, half_height_m), tall_skinny_dimensions); } function adjustSizeOfChatDiv( mode) { // Input fields dC.nodeServer = document.getElementById('nodeServer'); dC.roomName = document.getElementById('roomName'); dC.inputField = document.getElementById('inputField'); // The two divs that toggle dC.multiPlayer = document.getElementById('multiPlayer'); dC.ttcIntro = document.getElementById('ttcIntro'); var divW_Small = '420px'; var divW_Large = '540px'; var widthAdjust = -7; if (mode == 'small') { dC.nodeServer.style.width = (167 + widthAdjust) + 'px'; dC.roomName.style.width = (50 + 0) + 'px'; dC.inputField.style.width = (328 + widthAdjust) + 'px'; dC.ttcIntro.style.maxWidth = divW_Small; dC.ttcIntro.style.minWidth = divW_Small; dC.multiPlayer.style.maxWidth = divW_Small; dC.multiPlayer.style.minWidth = divW_Small; } else { dC.nodeServer.style.width = (267 + widthAdjust) + 'px'; dC.roomName.style.width = (70 + 0) + 'px'; dC.inputField.style.width = (448 + widthAdjust) + 'px'; dC.ttcIntro.style.maxWidth = divW_Large; dC.ttcIntro.style.minWidth = divW_Large; dC.multiPlayer.style.maxWidth = divW_Large; dC.multiPlayer.style.minWidth = divW_Large; } } function makeJello( pars) { var pinned = pars.pinned || false; var gridsize = pars.gridsize || 4; var offset_2d_m = new Vec2D(2.0, 2.0); var spacing_factor_m = 0.9; var v_init_2d_mps = new Vec2D(0.0, 0.0); var puckParms = {'radius_m':0.20, 'density':5.0, 'jello':true}; var springParms = { 'unstretched_width_m': 0.07, 'strength_Npm': 350.0, 'length_m': spacing_factor_m * 1.0, 'damper_Ns2pm2': 5.0}; // Grid of pucks. for (var j = 0; j < gridsize; j++) { for (var k = 0; k < gridsize; k++) { if ((j==2) && (k==2)) { puckParms.color = "orange"; } else { puckParms.color = null; } var pos_2d_m = new Vec2D( spacing_factor_m * j, spacing_factor_m * k); pos_2d_m.addTo( offset_2d_m); aT.jelloPucks.push( new Puck( pos_2d_m, v_init_2d_mps, puckParms)); } } // Horizontal springs (between neighbors) for (var m = 0; m < gridsize*(gridsize-1); m++) { springParms.color = "blue"; // Note: Object.assign is used here to make a copy of the springParms object (mutable). This avoids the multiple reference to springParms // and any associated mutation side effects (from this and the following color changes) when the state is captured. new Spring(aT.jelloPucks[m], aT.jelloPucks[m+gridsize], Object.assign({}, springParms)); } // Vertical springs for (var m = 0; m < gridsize-1; m++) { for (var n = 0; n < gridsize; n++) { var o_index = m + (n * gridsize); springParms.color = "blue"; new Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index+1], Object.assign({}, springParms)); } } // Diagonal springs for (var m = 0; m < gridsize-1; m++) { for (var n = 1; n < gridsize; n++) { var o_index = m + (n * gridsize); springParms.color = "yellow"; springParms.length_m = spacing_factor_m * 1.41; // A diagonal new Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index-(gridsize-1)], Object.assign({}, springParms)); } } // Diagonal springs (perpendicular to the other diagonals) for (var m = 0; m < gridsize-1; m++) { for (var n = 0; n < gridsize-1; n++) { var o_index = m + (n * gridsize); springParms.color = "yellow"; springParms.length_m = spacing_factor_m * 1.41; // A diagonal new Spring(aT.jelloPucks[o_index], aT.jelloPucks[o_index+(gridsize+1)], Object.assign({}, springParms)); } } // Add two pinned springs. if (pinned) { var corner_puck = (gridsize * gridsize) - 1; new Spring(aT.jelloPucks[ 0], new Pin( new Vec2D( 0.5, 0.5), {radius_px:4}), {strength_Npm:800.0, unstretched_width_m:0.3, color:'brown',damper_Ns2pm2:5.0}); new Spring(aT.jelloPucks[ corner_puck], new Pin( new Vec2D( 9.0, 9.0), {radius_px:4}), {strength_Npm:800.0, unstretched_width_m:0.3, color:'brown',damper_Ns2pm2:5.0}); } } function checkForJelloTangle() { // Determine if tangled by looking for balls that are fairly close to // each other. This does not require puck contact to detect a tangle. var diameter = 2 * aT.jelloPucks[0].radius_m; // A little more than the square of the diameter. var separation_check = Math.pow(diameter, 2) * 1.5; // A looping structure that avoids self reference and repeated puck-otherpuck references. for (var j = 0, len = aT.jelloPucks.length; j < len; j++) { for (var k = j+1; k < len; k++) { // Check distance between j and k pucks. var diff_2d_m = aT.jelloPucks[j].position_2d_m.subtract( aT.jelloPucks[k].position_2d_m); // Square of the vector length. var lenSquared = diff_2d_m.length_squared(); if (lenSquared < separation_check) { // This one is too close to be in a non-tangled jello block. //console.log(j + "," + k); c.tangleTimer_s += dt_frame_s; j = k = 10000; // get out of the loops. } } } ctx.font = "30px Arial"; ctx.fillStyle = 'yellow'; ctx.fillText(c.tangleTimer_s.toFixed(2),10,40); } /* Tried using the B2D contact listener to detect tangle. But this approach fails to deal with a tangled state where the balls are not quite touching... So the approach above is used. function checkForJelloTangle2() { if (c.contactCounter > 0) { c.tangleTimer_s += dt_frame_s; } ctx.font = "30px Arial"; ctx.fillStyle = 'yellow'; ctx.fillText(c.tangleTimer_s.toFixed(2),10,50); } */ function demoStart( index) { var v_init_2d_mps, wallColor; // Set this global to support the JSON capture. c.demoIndex = index; // Scaling factor between the Box2d world and the screen (pixels per meter) px_per_m = 100; // a global adjustSizeOfChatDiv('normal'); canvas.width = 600, canvas.height = 600; // Change the color of the button that was clicked. for (var j = 1; j <= 8; j++) { if (j == index) wallColor = "yellow"; else wallColor = "lightgray"; // goldenrod yellow document.getElementById('b'+j).style.backgroundColor = wallColor; } // Delete pucks (and references to them) from the previous demo. Puck.deleteAll(); // Clean out the old springs. Spring.deleteAll(); c.springNameForPasting = null; // Clean out the old pins and their representation in the b2d world. Pin.deleteAll(); // Clean out the old walls and their representation in the b2d world. Wall.deleteAll(); // De-select anything still selected. clients['local'].selectedBody = null; aT.multiSelectMap = {}; resetFenceColor( "white"); startit(); // if paused // Turn gravity off by default. if (c.g_ON) { c.g_ON = false; dC.gravity.checked = false; } resetFrictionParameters(); setGravityRelatedParameters(); // Convert (parse) the json capture into a local object. if (dC.json.value != '') { try { var state_capture = JSON.parse( dC.json.value); } catch (err) { var state_capture = null; window.alert("There's a formatting error in the state capture. Try clicking the 'Clear' button."); } } else { var state_capture = null; } if (index == 1) { scrollDemoHelp('#d1234'); if ((state_capture) && (state_capture.demoIndex == 1)) { restoreFromState( state_capture); } else { make_fence(); var v_init_2d_mps = new Vec2D(0.0, -2.0); new Puck( new Vec2D(2.0, 3.99), v_init_2d_mps, {'radius_m':0.10, 'color':'GoldenRod', 'colorSource':true}); new Puck( new Vec2D(2.0, 3.00), v_init_2d_mps, {'radius_m':0.80}); var v_init_2d_mps = new Vec2D(0.0, 2.0); new Puck( new Vec2D(5.00, 1.60+1.5*2), v_init_2d_mps, {'radius_m':0.35}); new Puck( new Vec2D(5.00, 1.60+1.5), v_init_2d_mps, {'radius_m':0.35, 'color':'GoldenRod', 'colorSource':true}); new Puck( new Vec2D(5.00, 1.60), v_init_2d_mps, {'radius_m':0.35}); new Puck( new Vec2D(0.50, 5.60), new Vec2D(0.40, 0.00), {'radius_m':0.15}); } } else if (index == 2) { scrollDemoHelp('#d2'); if ((state_capture) && (state_capture.demoIndex == 2)) { restoreFromState( state_capture); } else { make_fence(); new Puck( new Vec2D(4.5, 4.5), new Vec2D( 0.0, 0.0), {'radius_m':0.20, 'tailSwitch':true}); new Puck( new Vec2D(3.0, 3.0), new Vec2D( 0.0, 0.0), {'radius_m':0.60, 'color':'GoldenRod', 'colorSource':true, 'angularSpeed_rps':0.0}); new Puck( new Vec2D(1.5, 1.5), new Vec2D( 0.0, 0.0), {'radius_m':0.20, 'tailSwitch':true}); } } else if (index == 3) { scrollDemoHelp('#d1234'); c.restitution_gOn = 0.7; c.friction_gOn = 0.6; c.restitution_gOff = 1.0; c.friction_gOff = 0.0; v_init_2d_mps = new Vec2D(0.0, 2.0); if ((state_capture) && (state_capture.demoIndex == 3)) { restoreFromState( state_capture); } else { make_fence(); var grid_order = 7; var grid_spacing_m = 0.45; var startPosition_2d_m = new Vec2D(0.0, 0.0); for (var i = 1; i <= grid_order; i++) { for (var j = 1; j <= grid_order; j++) { var delta_2d_m = new Vec2D( i * grid_spacing_m, j * grid_spacing_m); var position_2d_m = startPosition_2d_m.add( delta_2d_m); new Puck(position_2d_m, v_init_2d_mps, {'radius_m':0.10}); } } v_init_2d_mps = new Vec2D(0.2, 0.0); new Puck( new Vec2D(5.5, 3.5), v_init_2d_mps, {'radius_m':0.10, 'color':'GoldenRod', 'colorSource':true} ); } } else if (index == 4) { scrollDemoHelp('#d4'); c.restitution_gOn = 0.7; c.friction_gOn = 0.6; c.restitution_gOff = 1.0; c.friction_gOff = 0.0; if ((state_capture) && (state_capture.demoIndex == 4)) { restoreFromState( state_capture); } else { make_fence(); new Puck( new Vec2D(3.00, 3.00), new Vec2D( 0.0, 0.0), {'radius_m':0.40, 'color':'GoldenRod', 'colorSource':true , 'shape':'rect', 'angularSpeed_rps':25.0}); new Puck( new Vec2D(0.25, 3.00), new Vec2D( 2.0, 0.0), {'radius_m':0.15, 'shape':'rect', 'aspectR':4.0, 'angularSpeed_rps':0, 'angle_r': Math.PI/2}); new Puck( new Vec2D(5.75, 3.00), new Vec2D(-2.0, 0.0), {'radius_m':0.15, 'shape':'rect', 'aspectR':4.0, 'angularSpeed_rps':0, 'angle_r': Math.PI/2}); } } else if (index == 5) { scrollDemoHelp('#d5'); c.restitution_gOn = 0.7; c.friction_gOn = 0.6; c.restitution_gOff = 1.0; c.friction_gOff = 0.0; v_init_2d_mps = new Vec2D(0.0,0.0); if ((state_capture) && (state_capture.demoIndex == 5)) { restoreFromState( state_capture); } else { make_fence(); // Spring triangle. var tri_vel_mps = new Vec2D( 6.0, 0.0); new Puck( new Vec2D(1.00, 0.70 + Math.sin(60.0*Math.PI/180)), tri_vel_mps, {'radius_m':0.20, 'name': 'puck1', 'restitution':0.0}); tri_vel_mps.rotated_by(-240.0); new Puck( new Vec2D(0.50, 0.70 ), tri_vel_mps, {'radius_m':0.20, 'name': 'puck2', 'restitution':0.0}); tri_vel_mps.rotated_by(-240.0); new Puck( new Vec2D(1.50, 0.70 ), tri_vel_mps, {'radius_m':0.20, 'name': 'puck3', 'restitution':0.0}); new Spring(aT.puckMap['puck1'], aT.puckMap['puck2'], {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':'blue'}); new Spring(aT.puckMap['puck1'], aT.puckMap['puck3'], {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':'blue'}); new Spring(aT.puckMap['puck2'], aT.puckMap['puck3'], {'length_m':1.1, 'strength_Npm':60.0, 'unstretched_width_m':0.1, 'color':'blue'}); // A spring with one puck and one pin. new Puck( new Vec2D(4.0, 5.0), new Vec2D(5.0, 0.0), {'radius_m':0.55, 'name':'puck4', 'restitution':0.0}); new Spring(aT.puckMap['puck4'], new Pin( new Vec2D( 4.0, 4.0),{}), {'strength_Npm':20.0, 'unstretched_width_m':0.2, 'color':'yellow', 'damper_Ns2pm2':1.0}); // Two pucks (one bigger than the other) on spring orbiting each other (upper left corner) new Puck( new Vec2D(0.75, 5.00), new Vec2D(0.0, -5.00 * 1.2), {'radius_m':0.15, 'name':'puck5'}); new Puck( new Vec2D(1.25, 5.00), new Vec2D(0.0, 1.80 * 1.2), {'radius_m':0.25, 'name':'puck6'}); new Spring(aT.puckMap['puck5'], aT.puckMap['puck6'], {'length_m':0.5, 'strength_Npm':5.0, 'unstretched_width_m':0.05, 'color':'yellow'}); // Same thing (lower right corner) new Puck( new Vec2D(5.00, 0.55), new Vec2D(+5.00, 0.0), {'radius_m':0.20, 'name':'puck7'}); new Puck( new Vec2D(5.00, 1.55), new Vec2D(-5.00, 0.0), {'radius_m':0.20, 'name':'puck8'}); new Spring(aT.puckMap['puck7'], aT.puckMap['puck8'], {'length_m':0.5, 'strength_Npm':5.0, 'unstretched_width_m':0.05, 'color':'yellow'}); } } else if (index == 6) { scrollDemoHelp('#d6'); c.g_ON = false; dC.gravity.checked = false; c.restitution_gOn = 0.0; c.friction_gOn = 0.6; c.restitution_gOff = 0.0; c.friction_gOff = 0.6; c.tangleTimer_s = 0.0; if ((state_capture) && (state_capture.demoIndex == 6)) { restoreFromState( state_capture); } else if ( demo_6_fromFile) { restoreFromState( demo_6_fromFile); } else { make_fence(); makeJello({}); } setGravityRelatedParameters(); // An extra puck to play with. //puckParms.restitution = 0.0; //new Puck( 3.8, 5.5, v_init_2d_mps, puck_radius_m * 2.8, puckParms); } else if (index == 7) { scrollDemoHelp('#d7'); if ((state_capture) && (state_capture.demoIndex == 7)) { restoreFromState( state_capture); } else { make_fence(); // Normal pucks new Puck( new Vec2D(0.35, 0.35), new Vec2D( 0.0, 4.0), {'radius_m':0.25}); // , 'categoryBits':'0x0000', 'maskBits':'0x0000', 'color':'pink' new Puck( new Vec2D(5.65, 0.35), new Vec2D( 0.0, 4.0), {'radius_m':0.25}); // , 'categoryBits':'0x0000', 'maskBits':'0x0000', 'color':'pink' new Puck( new Vec2D(2.75, 0.35), new Vec2D(+2.0, 0.0), {'radius_m':0.25}); new Puck( new Vec2D(3.25, 0.35), new Vec2D(-2.0, 0.0), {'radius_m':0.25}); new Puck( new Vec2D(0.35, 5.65), new Vec2D(+2.0, 0.0), {'radius_m':0.25}); new Puck( new Vec2D(5.65, 5.65), new Vec2D(-2.0, 0.0), {'radius_m':0.25}); } // Make a controlled puck for each client. for (var clientName in clients) { new Puck( new Vec2D(3.0, 5.5), new Vec2D(0.0, -2.5), {'radius_m':0.30, 'color':'black', 'colorSource':true, 'clientName':clientName, 'linDamp':1.0, 'hitLimit':20} ); } } else if (index == 8) { canvas.width = 1250, canvas.height = 950; adjustSizeOfChatDiv('small'); // Must do this after the chat-div adjustment. scrollDemoHelp('#d8'); c.g_ON = false; dC.gravity.checked = false; c.restitution_gOn = 0.0; //0.7 c.friction_gOn = 0.6; c.restitution_gOff = 0.0; //1.0 c.friction_gOff = 0.6; setGravityRelatedParameters(); if ((state_capture) && (state_capture.demoIndex == 8)) { restoreFromState( state_capture); } else if (demo_8_fromFile) { // Don't need to parse here because read in from a file. restoreFromState( demo_8_fromFile); // Some little walls in the middle. /* new Wall( new Vec2D( 2.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14}); new Wall( new Vec2D( 3.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 4.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2}); new Wall( new Vec2D( 5.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 6.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14}); new Wall( new Vec2D( 7.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 8.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2}); */ } else { makeJello({'pinned':true, 'gridsize':4}); make_fence(); // Some little walls in the middle. new Wall( new Vec2D( 2.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02, 'angularSpeed_rps':3.14/2}); new Wall( new Vec2D( 3.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 4.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 5.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 6.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 7.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); new Wall( new Vec2D( 8.0, 0.5), {'half_width_m':0.4, 'half_height_m':0.02}); } // Make a controlled puck for each client. Randomize the position and initial velocity. for (var clientName in clients) { new Puck( new Vec2D( (meters_from_px( canvas.width)-0.3) * Math.random(), (meters_from_px( canvas.height)-0.3) * Math.random() ), new Vec2D( 15.0 * (Math.random()-0.5), 15.0 * (Math.random()-0.5)), {'radius_m':0.30, 'color':'black', 'colorSource':true, 'clientName':clientName, 'linDamp':1.0, 'hitLimit':20} ); } } else if (index == 9) { } else if (index == 0) { } } // Initialize almost everything ///////////////////////////////////////////// function init() { // Make a world in Box2D // Constraint on space in world worldAABB = new b2AABB(); worldAABB.lowerBound.Set(-20.0, -20.0); worldAABB.upperBound.Set( 20.0, 20.0); // b2d world: set gravity vector to 0, allow sleep. world = new b2World( new b2Vec2(0, -0.0), true); // Event handlers for Box2D. Get collision information. var listener = new Box2D.Dynamics.b2ContactListener; listener.BeginContact = function (contact) { // Use the table map to get a reference back to a gW object. var body_A = tableMap.get(contact.GetFixtureA().GetBody()); var body_B = tableMap.get(contact.GetFixtureB().GetBody()); //console.log(""); //console.log("A=" + body_A.constructor.name); //console.log("B=" + body_B.constructor.name); // Set the wall color to that of the puck hitting it. if (body_A.constructor.name == "Wall" || body_B.constructor.name == "Wall") { if (body_B.constructor.name == "Puck") { var body_Puck = body_B, body_Wall = body_A; } else { var body_Puck = body_A, body_Wall = body_B; } // If it's a puck designated as a color source, use its client color for the wall. if (body_Puck.colorSource) { if (body_Puck.clientName && body_Wall.fence) { body_Wall.color = clients[body_Puck.clientName].color; } else if (body_Wall.fence) { body_Wall.color = body_Puck.color; } } else { // Reset the wall color to it's default. body_Wall.color = Wall.color_default; } } else if (body_A.constructor.name == "Puck" && body_B.constructor.name == "Puck") { //c.contactCounter++; if (body_A.bullet && !body_B.bullet) { // Can't shoot yourself in the foot. if (body_A.createdByClient != body_B.clientName) { if (!body_B.shield.ON || (body_B.shield.ON && !body_B.shield.STRONG)) { body_B.hitCount += 1; body_B.flash = true; } } } else if (body_B.bullet && !body_A.bullet) { if (body_B.createdByClient != body_A.clientName) { if (!body_A.shield.ON || (body_A.shield.ON && !body_A.shield.STRONG)) { body_A.hitCount += 1; body_B.flash = true; } } } } } /* listener.EndContact = function (contact) { // Use the table map to get a reference back to a gW object. var body_A = tableMap.get(contact.GetFixtureA().GetBody()); var body_B = tableMap.get(contact.GetFixtureB().GetBody()); if (body_A.constructor.name == "Puck" && body_B.constructor.name == "Puck") { c.contactCounter--; } } */ world.SetContactListener(listener); // Initialize the canvas display window. myRequest = null; resumingAfterPause = false; time_previous = performance.now(); // Initialize the previous time variable to now. canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d'); // Miscellaneous pointers to DOM elements dC.json = document.getElementById('jsonCapture'); // Event handlers for local client (user input) // Inhibit the context menu that pops up when right clicking (third button). // Do this on mainDiv to prevent the menu from appearing when you drag the // mouse off the canvas. var mainDiv = document.getElementById('mainDiv'); mainDiv.addEventListener("contextmenu", function(e) { //console.log('contextmenu event'); e.preventDefault(); return false; }, {capture: false}); canvas.addEventListener("mousedown", function(e) { clients['local'].isMouseDown = true; clients['local'].button = e.button; // Start a listener for the mousemove event. document.addEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); // Pass this first mouse position to the move handler. This will establish // the world position of the mouse. handleMouseOrTouchMove(e); // (Note: also see the checkForMouseSelection method in the Client prototype.) // Check for body at the mouse position. If nothing there, and shift key is UP, reset the // multi-select map. So, user needs to release the shift key and click on open area to // flush out the multi-select. var selected_b2d_Body = b2d_getBodyAt( clients['local'].mouse_2d_m); var selectedBody = tableMap.get( selected_b2d_Body); if (clients['local'].key_shift == "U") { // Un-dash all the springs. Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = false; }); // Clicked on blank space on air table (un-selecting everything) if (!selected_b2d_Body) { // Un-select each spring that is within the multi-select map. aT.multiSelectMap = {}; } } // This prevents the middle mouse button from doing scrolling operations. e.preventDefault(); }, {capture: false}); canvas.addEventListener("touchstart", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; clients['local'].isMouseDown = true; clients['local'].button = 0; // Start a listener for the touchmove event. document.addEventListener("touchmove", handleMouseOrTouchMove, {passive: true, capture: false}); //Pass this first mouse position to the move handler. handleMouseOrTouchMove(e); }, {passive: true, capture: false}); function handleMouseOrTouchMove(e) { // Determine if mouse or touch. if (e.clientX) { // Mouse var raw_x_px = e.clientX; var raw_y_px = e.clientY; } else if (e.touches) { // Touch var raw_x_px = e.touches[0].clientX; var raw_y_px = e.touches[0].clientY; } clients['local'].mouseX_px = raw_x_px - canvas.getBoundingClientRect().left; clients['local'].mouseY_px = raw_y_px - canvas.getBoundingClientRect().top; clients['local'].mouse_2d_px = new Vec2D(clients['local'].mouseX_px, clients['local'].mouseY_px); // A tweak to make the cursor location be at the very tip of the arrow icon. var cursorCorrection_2d_px = new Vec2D(-5,-4); clients['local'].mouse_2d_px.addTo( cursorCorrection_2d_px); clients['local'].mouse_2d_m = worldFromScreen( clients['local'].mouse_2d_px); //console.log("x,y=" + clients['local'].mouse_2d_m.x + "," + clients['local'].mouse_2d_m.y); }; document.addEventListener("mouseup", function(e) { if (!clients['local'].isMouseDown) return; // Stop (using cpu) watching the mouse position. document.removeEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); resetMouseOrFingerState(e); }, {capture: false}); canvas.addEventListener("touchend", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; if (!clients['local'].isMouseDown) return; // Stop (using cpu) watching the position. document.removeEventListener("touchmove", handleMouseOrTouchMove, {passive: true, capture: false}); resetMouseOrFingerState(e); }, {passive: true, capture: false}); function resetMouseOrFingerState(e) { clients['local'].isMouseDown = false; clients['local'].button = null; clients['local'].mouseX_m = null; clients['local'].mouseY_m = null; } var arrowKeysMap = {'key_leftArrow':'thinner', 'key_rightArrow':'wider', 'key_upArrow':'taller', 'key_downArrow':'shorter'}; document.addEventListener("keydown", function(e) { //console.log(e.keyCode + "(down/repeated)=" + String.fromCharCode(e.keyCode)); // The following is necessary in Firefox to avoid the spacebar from re-clicking // page controls (like the demo buttons) if they have focus. if (document.activeElement != document.body) document.activeElement.blur(); // Anything at this outer level will repeat if the key is held down. if (e.keyCode in keyMap) { // If you want down keys to repeat, put them here. // This allows the spacebar to be used for the puck shields. if (keyMap[e.keyCode] == 'key_space') { // Inhibit page scrolling that results from using the spacebar. e.preventDefault(); } else if (keyMap[e.keyCode] in arrowKeysMap) { // Note: if arrowKeys is an array, instead of an object map, // the check can be done this way: (arrowKeys.indexOf( keyMap[e.keyCode]) != -1). // Prevent page scrolling when using the arrow keys in the editor. e.preventDefault(); } // Control body angle when editing. if (keyMap[e.keyCode] == 'key_z') { if (clients['local'].key_shift == 'D') { // Rotate counterclockwise var angle_change_d = +2; // degrees } else { // Rotate clockwise var angle_change_d = -2; // degrees } if (clients['local'].selectedBody) { var current_angle_r = clients['local'].selectedBody.b2d.GetAngle(); var new_angle_r = current_angle_r + angle_change_d*(Math.PI/180); clients['local'].selectedBody.angle_r = new_angle_r; clients['local'].selectedBody.b2d.SetAngle( new_angle_r); } } // Change body rotation when editing. if (keyMap[e.keyCode] == 'key_t') { if (clients['local'].key_shift == 'D') { // Increase rate counterclockwise var rotRate_change_dps = +5; // degrees per second } else { // Increase rate clockwise var rotRate_change_dps = -5; // degrees per second } if (clients['local'].selectedBody) { var current_rotRate_rps = clients['local'].selectedBody.b2d.GetAngularVelocity(); var new_rotRate_rps = current_rotRate_rps + rotRate_change_dps*(Math.PI/180); clients['local'].selectedBody.angularSpeed_rps = new_rotRate_rps; clients['local'].selectedBody.b2d.SetAngularVelocity( new_rotRate_rps); if (clients['local'].selectedBody.constructor.name == "Wall") { // If not currently rotating, will need to delete and recreate the body. This is // an oddity of b2d in that you can't change the rotation rate on an existing kinematic body that currently // is NOT rotating. if (current_rotRate_rps == 0.0) { // Make a temporary reference to the selected body. var oldWall = clients['local'].selectedBody; // Delete the selected wall. clients['local'].selectedBody.deleteThisOne(); // Point the client reference to a new wall. Rebuild the wall at the new rotational rate (all other parameters are equal to those of the old wall). clients['local'].selectedBody = new Wall( oldWall.position_2d_m, {'half_width_m':oldWall.half_width_m, 'half_height_m':oldWall.half_height_m, 'angle_r':oldWall.angle_r, 'angularSpeed_rps':new_rotRate_rps}); } } } } // Use the arrow keys to change the dimensions of the selected body. if (keyMap[e.keyCode] in arrowKeysMap) { // Multi-select if (Object.keys(aT.multiSelectMap).length > 0) { // Springs if (clients['local'].key_s == 'D'){ Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]); }); // All other object types } else { for (var objName in aT.multiSelectMap) { aT.multiSelectMap[ objName].modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]) } } } // Single-body selection (client string) if (clients['local'].selectedBody) { clients['local'].selectedBody.modify_fixture( arrowKeysMap[ keyMap[e.keyCode]]); } } // Keys that are held down will NOT repeat in this next block. // This is for cases where you are toggling the state of the client's // key parameter. // If the current key state is UP... if (clients['local'][keyMap[e.keyCode]] == 'U') { // Set the key state to be DOWN. clients['local'][keyMap[e.keyCode]] = 'D'; //console.log(e.keyCode + "(down)=" + keyMap[e.keyCode]); // Immediate execution on keydown (that's the event that got you in here.): if ((keyMap[e.keyCode] == 'key_c') && (clients['local'].key_ctrl != 'D')) { dC.comSelection.checked = !dC.comSelection.checked; comSelection_Toggle(); } else if (keyMap[e.keyCode] == 'key_f') { freeze(); } else if (keyMap[e.keyCode] == 'key_r') { stopRotation(); } else if (keyMap[e.keyCode] == 'key_g') { c.g_ON = !c.g_ON; if (c.g_ON) { dC.gravity.checked = true; } else { dC.gravity.checked = false; } setGravityRelatedParameters(); /* // If there is only one fixture, m_fixtureList (a linked list) is a reference to that single fixture. console.log(' '); console.log("fixture count=" + aT.wallMap['wall1'].b2d.m_fixtureCount); // also might want to look here: m_fixtureList, m_fixtureList.m_shape, m_fixtureList.m_shape.m_vertices for (var x in aT.wallMap['wall1'].b2d.m_fixtureList) { console.log("name=" + x); } */ } else if (keyMap[e.keyCode] == 'key_m') { dC.multiplayer.checked = !dC.multiplayer.checked; toggleMultiplayerStuff(); } else if (keyMap[e.keyCode] == 'key_e') { dC.editor.checked = !dC.editor.checked; toggleEditorStuff(); } else if (keyMap[e.keyCode] == 'key_p') { dC.pause.checked = !dC.pause.checked; pause_Toggle(); // Delete stuff } else if ((keyMap[e.keyCode] == 'key_x') && (clients['local'].key_ctrl == 'D')) { // First process multi-select var foundSpring = false; if (Object.keys(aT.multiSelectMap).length > 0) { // Delete each spring that has both it's pucks (or pins) in the multi-select. Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].deleteThisOne(); // This function includes the scope of the function in which is being defined. // So foundSpring, defined in the surrounding function, is accessible (and changeable) here. foundSpring = true; }); // If springs have been cleared during first delete, now remove pucks, pins and walls that are still selected. //console.log('foundSpring='+foundSpring); if (!foundSpring) { for (var name_multiSelect in aT.multiSelectMap) { aT.multiSelectMap[ name_multiSelect].deleteThisOne(); delete aT.multiSelectMap[ name_multiSelect]; } } } else if (clients['local'].selectedBody) { // A single-object selection. clients['local'].selectedBody.deleteThisOne(); // Pucks, pins, and walls all have there own version of this method. clients['local'].selectedBody = null; } // Copy stuff } else if ((keyMap[e.keyCode] == 'key_c') && (clients['local'].key_ctrl == 'D')) { // Copy a Spring for pasting. // First deal with multi-select case (a length of 2 indicates trying to copy a spring) if (Object.keys(aT.multiSelectMap).length == 2) { // Make a copy of the spring (if there is one connected to these two objects). Spring.findInMultiSelect( function( springName) { // Make a reference to this existing spring. c.springNameForPasting = springName; }); // Normal copying of an object that is identified by single-object selection } else if (clients['local'].selectedBody) { var cn = clients['local'].selectedBody.constructor.name; if ((cn == "Wall") || (cn == "Pin") || (cn == "Puck")) { clients['local'].selectedBody.copyThisOne(); } } // Paste a spring onto a pair of pucks. } else if ((keyMap[e.keyCode] == 'key_v') && (clients['local'].key_ctrl == 'D')) { if (Object.keys(aT.multiSelectMap).length == 2) { var p = []; // Populate this little array so you can pass the pucks as parameters. for (var p_name in aT.multiSelectMap) { p.push( aT.multiSelectMap[ p_name]); } if (c.springNameForPasting in aT.springMap) { // Check if there's already a spring between these two. Delete it // before pasting. Spring.findInMultiSelect( function( springName) { if (aT.springMap[ springName]) aT.springMap[ springName].deleteThisOne(); }); // Paste a copy of the spring on these two pucks (or pins). aT.springMap[ c.springNameForPasting].copyThisOne( p[0], p[1]); } } } } /* Could have a second block for keys that repeat while held down. In this case you wouldn't require the UP state to execute. */ // numbers 0 to 9 } else if ((e.keyCode >= 48) && (e.keyCode <= 57)) { demoStart(e.keyCode - 48); } }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase). document.addEventListener("keyup", function(e) { if (e.keyCode in keyMap) { // Set the key state to be UP. clients['local'][keyMap[e.keyCode]] = 'U'; //console.log(e.keyCode + "(up)=" + keyMap[e.keyCode]); // numbers 0 to 9 } else if ((e.keyCode >= 48) && (e.keyCode <= 57)) { } }, {capture: false}); //This "false" makes this fire in the bubbling phase (not capturing phase). // Gravity toggle dC.gravity = document.getElementById('chkGravity'); function gravityToggle(e) { if (dC.gravity.checked) { c.g_ON = true; } else { c.g_ON = false; } setGravityRelatedParameters(); } dC.gravity.addEventListener("click", gravityToggle, {capture: false}); // COM (Center of Mass) selection toggle dC.comSelection = document.getElementById('chkCOM_Selection'); function comSelection_Toggle(e) { if (dC.comSelection.checked) { } else { } } dC.comSelection.addEventListener("click", comSelection_Toggle, {capture: false}); // Multi-player toggle dC.multiplayer = document.getElementById('chkMultiplayer'); dC.multiplayer.addEventListener("click", toggleMultiplayerStuff, {capture: false}); // Editor toggle dC.editor = document.getElementById('chkEditor'); function toggleEditorStuff() { toggleElementDisplay("editControls", "inline"); } dC.editor.addEventListener("click", toggleEditorStuff, {capture: false}); // Pause toggle dC.pause = document.getElementById('chkPause'); function pause_Toggle(e) { if (dC.pause.checked) { stopit(); } else { startit(); } } dC.pause.addEventListener("click", pause_Toggle, {capture: false}); // The running average. aT.dt_RA_ms = new RunningAverage(60); dC.fps = document.getElementById("fps"); // Add a local user to the clients dictionary. clients['local'] = new Client({'name':'local'}); // Start the first demo. demoStart( 2); } // It's alive. MuuuUUuuuAhhhh Ha Ha Ha. function gameLoop( timeStamp_ms) { // Note: The time-stamp argument can have any name. dt_frame_ms = timeStamp_ms - time_previous; //dt_frame_ms = c.deltaT_s * 1000; //dt_frame_ms = 1000 * 1/60.0 dt_frame_s = dt_frame_ms / 1000.0; if (resumingAfterPause || (dt_frame_s > 0.1)) { // Use the dt info saved in last frame before it was paused. dt_frame_ms = dt_frame_previous_ms; dt_frame_s = dt_frame_ms / 1000.0; time_previous = performance.now(); resumingAfterPause = false; } if (c.dtFloating) c.deltaT_s = dt_frame_s; var dt_avg_ms = aT.dt_RA_ms.update( dt_frame_ms); // Report frame-rate every half second. if (aT.dt_RA_ms.totalSinceReport > 500.0) { dC.fps.innerHTML = (1/(dt_avg_ms/1000)).toFixed(0); aT.dt_RA_ms.totalSinceReport = 0.0; } // Draw the walls, step the engine, draw the pucks. updateAirTable(); //console.log("timeStamp_ms = " + timeStamp_ms); //console.log("performance.now = " + performance.now()); //console.log("dt_frame_ms = " + dt_frame_ms.toFixed(2) + " ms"); time_previous = timeStamp_ms; dt_frame_previous_ms = dt_frame_ms //console.log("5"); myRequest = window.requestAnimationFrame( gameLoop); } function updateAirTable() { // Clear the canvas (from one corner to the other) ctx.fillStyle = "black"; ctx.fillRect(0,0, canvas.width, canvas.height); //ctx.clearRect(0,0, canvas.width, canvas.height); // Calculate the state of the objects. world.Step( c.deltaT_s, 10, 10); // dt_frame_s c.deltaT_s world.ClearForces(); // Draw the walls first (render these on the bottom). for (var wallName in aT.wallMap) { aT.wallMap[ wallName].draw(); } /* Leaving this commented block here as an example of a technique for deleting elements from an array when looping over it. // Clean out old bullets and unhealthy pucks. Note this loops // in reverse order over the array to avoid indexing problems as the // array elements are deleted. for (var j = aT.pucks.length - 1; j >= 0; j--) { if (aT.pucks[j].bullet) { var age_ms = window.performance.now() - aT.pucks[j].createTime; if (age_ms > aT.pucks[j].ageLimit_ms) { deletePuckAndParts( aT.pucks[j]); aT.pucks.splice(j, 1); } } else if (aT.pucks[j].poorHealthFraction >= 1.0) { deletePuckAndParts( aT.pucks[j]); aT.pucks.splice(j, 1); } } */ // Clean out old bullets and unhealthy pucks. for (var puckName in aT.puckMap) { if (aT.puckMap[ puckName].bullet) { var age_ms = window.performance.now() - aT.puckMap[ puckName].createTime; if (age_ms > aT.puckMap[ puckName].ageLimit_ms) { aT.puckMap[ puckName].deleteThisOne(); } } else if (aT.puckMap[ puckName].poorHealthFraction >= 1.0) { aT.puckMap[ puckName].deleteThisOne(); } } for (var puckName in aT.puckMap) { aT.puckMap[ puckName].updateState(); aT.puckMap[ puckName].draw(); } for (var springName in aT.springMap) { var theSpring = aT.springMap[ springName]; if (theSpring.spo1.deleted || theSpring.spo2.deleted) { // If either puck has been deleted, remove the spring (and pin if pinned). // If the spring is pinned, remove the pin from the pin map. if (theSpring.pinned) { var thePin = aT.pinMap[ theSpring.p2_name]; // Delete the pin, it's b2d object, and the reference to it. if (thePin) thePin.deleteThisOne(); } // Remove this spring from the spring map. aT.springMap[ springName].deleteThisOne(); } else { // Otherwise, business as usual. theSpring.force_on_pucks(); theSpring.draw(); } } for (var pinName in aT.pinMap) { aT.pinMap[ pinName].draw(); } // Check for jello tangle if (c.demoIndex == 6) { if (aT.jelloPucks.length > 0) checkForJelloTangle(); //checkForJelloTangle2(); } // Jets and Guns for (var clientName in clients) { if (clients[clientName].puck) { // Respond to client controls, calculate corresponding jet and gun recoil forces, and draw. clients[clientName].puck.jet.updateAndDraw(); clients[clientName].puck.gun.updateAndDraw(); } } // Draw the client strings last (so they render on top). for (var clientName in clients) { clients[clientName].checkForMouseSelection(); if (clients[clientName].selectedBody) { clients[clientName].updateSelectionPoint(); clients[clientName].drawSelectionString(); } if (clientName != 'local') { // Draw a cursor for the network clients. clients[clientName].drawCursor(); } } // Draw a marking circle on each object in the multi-select map. if (Object.keys(aT.multiSelectMap).length > 0) { for (var multiSelect_name in aT.multiSelectMap) { aT.multiSelectMap[ multiSelect_name].draw_MultiSelectPoint(); } } Spring.findInMultiSelect( function( springName) { aT.springMap[ springName].dashedLine = true; }); // Consider all client-mouse influences on a selected object. for (var clientName in clients) { if (clients[clientName].selectedBody) { var bodyType = clients[clientName].selectedBody.b2d.GetType(); // Pucks if (bodyType == b2Body.b2_dynamicBody) { clients[clientName].calc_string_forces_on_puck(); // Walls } else if (bodyType == b2Body.b2_kinematicBody) { // Move the selected wall to the cursor location. clients[clientName].moveToCursorPosition(); } } } // Sum up all the forces and apply them to the pucks. for (var puckName in aT.puckMap) { aT.puckMap[ puckName].applyForces(); } } // Reveal public pointers to private functions and properties /////////////// return { startit: startit, stopit: stopit, setFrameRate: setFrameRate, init: init, freeze: freeze, stopRotation: stopRotation, createNetworkClient: createNetworkClient, deleteNetworkClient: deleteNetworkClient, updateClientState: updateClientState, toggleElementDisplay: toggleElementDisplay, toggleSpanValue: toggleSpanValue, saveState: saveState, clearState: clearState, scrollDemoHelp: scrollDemoHelp, adjustSizeOfChatDiv: adjustSizeOfChatDiv, openDemoHelp: openDemoHelp, demoStart: demoStart }; })();