/*
Copyright 2022 James D. Miller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Constructors and Prototypes (cP) module
// constructorsAndPrototypes.js
console.log('CP version 0.0');
// 4:04 PM Wed August 10, 2022
/*
Dependencies for constructorsAndPrototypes.js:
gwModule.js (gW.)
hostAndClient.js (hC.)
utilities.js
*/
var cP = (function() {
"use strict";
// Short names for Box2D constructors and prototypes
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,
b2DistanceJointDef = Box2D.Dynamics.Joints.b2DistanceJointDef,
b2DistanceJoint = Box2D.Dynamics.Joints.b2DistanceJoint,
b2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef,
b2RevoluteJoint = Box2D.Dynamics.Joints.b2RevoluteJoint,
b2MassData = Box2D.Collision.Shapes.b2MassData,
b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape,
b2CircleShape = Box2D.Collision.Shapes.b2CircleShape,
b2AABB = Box2D.Collision.b2AABB;
var dF = new DrawingFunctions();
/////////////////////////////////////////////////////////////////////////////
////
//// Object Constructors and Prototypes
////
/////////////////////////////////////////////////////////////////////////////
function Vec2D(x, y) {
this.x = x;
this.y = y;
}
Vec2D.prototype.copy = function() {
return new Vec2D( this.x, this.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.normal = function() {
var length = this.length();
var x = this.x / length;
var y = this.y / length;
return new Vec2D(x, 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) {
// Rotate relative to the current orientation
// angle_degrees is the change in the angle, from current to new.
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.equal = function( p_2d) {
if ((this.x == p_2d.x) && (this.y == p_2d.y)) {
return true;
} else {
return false;
}
}
Vec2D.prototype.zeroLength = function() {
if ((this.x == 0) && (this.y == 0)) {
return true;
} else {
return false;
}
}
Vec2D.prototype.length_squared = function() {
return (this.x*this.x + this.y*this.y);
}
Vec2D.prototype.get_angle = function() {
// Determine the angle (in degrees) that this vector makes with the x axis. Measure
// counterclockwise from the x axis.
if (this.length_squared() == 0) {
return 0;
} else {
// Yes, this is correct, y is the first parameter.
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);
}
Vec2D.prototype.angleBetweenPoints_r = function( p1_2d, p2_2d) {
// Find the angle formed by the two vectors that originate at this vector, with end points at p1 and p2.
// Angle (degrees relative to x axis) of the differential vector between this vector and p1_2d.
var angle_1_d = p1_2d.subtract(this).get_angle();
// Angle (degrees relative to x axis) of the differential vector between this vector and p2_2d.
var angle_2_d = p2_2d.subtract(this).get_angle();
var delta_d = angle_2_d - angle_1_d;
// Change in angle (radians) from p1 to p2.
var delta_r = delta_d * (Math.PI/180.0);
return delta_r;
}
Vec2D.prototype.angleBetweenVectors_d = function( vector_2d) {
var angle_1_d = this.get_angle();
var angle_2_d = vector_2d.get_angle();
var delta_d = angle_2_d - angle_1_d;
return delta_d;
}
Vec2D.prototype.matchAngle = function( p_2d) {
var newAngle_d = p_2d.get_angle();
this.set_angle( newAngle_d);
return newAngle_d;
}
function HSLColor( pars = {}) {
this.hue = setDefault( pars.hue, 180); // 0 to 360
this.saturation = setDefault( pars.saturation, 50); // 0 to 100 (%)
this.lightness = setDefault( pars.lightness, 50); // 0 to 100 (%)
this.stepDirection = 1; // or -1
this.stepSize = setDefault( pars.stepSize, 2);
this.steppingKey = setDefault( pars.steppingKey, 'lightness');
this.stepping = setDefault( pars.stepping, true);
}
HSLColor.prototype.parse = function( hslString) {
// example hsl string: 'hsl(162, 11%, 81%)'
let regexp = /hsl\(\s*(\d+)\s*,\s*(\d+(?:\.\d+)?%)\s*,\s*(\d+(?:\.\d+)?%)\)/g;
let result = regexp.exec( hslString).slice(1);
this.hue = Number( result[0]);
this.saturation = Number( result[1].slice(0,-1)); // remove the % sign at the end.
this.lightness = Number( result[2].slice(0,-1)); // and here too...
}
HSLColor.prototype.step = function() {
if (this.stepping) {
if (this.steppingKey == 'hue') {
if ((this.hue < 0) || (this.hue > 360)) this.stepDirection *= -1;
this.hue += this.stepSize * this.stepDirection;
} else if (this.steppingKey == 'saturation') {
if ((this.saturation < 0) || (this.saturation > 100)) this.stepDirection *= -1;
this.saturation += this.stepSize * this.stepDirection;
} else if (this.steppingKey == 'lightness') {
if ((this.lightness < 0) || (this.lightness > 100)) this.stepDirection *= -1;
this.lightness += this.stepSize * this.stepDirection;
}
}
}
HSLColor.prototype.colorString = function() {
let hslString = "hsl(" + this.hue + ", " + this.saturation + "%, " + this.lightness + "%)";
return String( hslString);
}
function HelpMessage( pars) {
this.message = setDefault( pars.message, "");
this.timeLimit_s = setDefault( pars.timeLimit_s, 2.0);
this.font = setDefault( pars.font, "20px Arial");
this.lineHeight_px = parseInt(this.font.substring(0,3)) * 1.20;
this.color = setDefault( pars.color, 'yellow');
this.loc_px = setDefault( pars.loc_px, {x:30, y:40});
this.messageSeries = null;
this.index = 0;
this.timeType = setDefault( pars.timeType, 'system'); //'game'
this.birthTime = window.performance.now();
this.time_s = 0.0;
this.popAtEnd = setDefault( pars.popAtEnd, false);
}
HelpMessage.prototype.setFont = function( fontString) {
this.font = fontString;
this.lineHeight_px = parseInt(this.font.substring(0,3)) * 1.20;
}
HelpMessage.prototype.resetMessage = function() {
this.message = "";
this.messageSeries = null;
}
HelpMessage.prototype.newMessage = function( message, timeLimit_s) {
this.time_s = 0.0;
this.birthTime = window.performance.now();
this.timeLimit_s = setDefault( timeLimit_s, this.timeLimit_s)
this.message = message;
}
HelpMessage.prototype.newMessageSeries = function( message) {
this.messageSeries = message;
// Initialize the first message.
this.time_s = 0.0;
this.birthTime = window.performance.now();
this.index = 1;
this.message = this.messageSeries[this.index].message;
this.timeLimit_s = this.messageSeries[this.index].tL_s;
}
HelpMessage.prototype.getDurationOfSeries_s = function() {
let seriesTimeTotal_s = 0;
for (let index in this.messageSeries) {
let messageObj = this.messageSeries[ index];
seriesTimeTotal_s += messageObj.tL_s;
}
return seriesTimeTotal_s;
}
HelpMessage.prototype.addToIt = function( moreText, pars = {}) {
this.timeLimit_s += setDefault( pars.additionalTime_s, 0);
this.message += moreText;
}
HelpMessage.prototype.yMax_px = function() {
var nLines = this.message.split("\\").length;
var yMax_px = this.loc_px.y + (nLines * this.lineHeight_px);
return yMax_px;
}
HelpMessage.prototype.displayIt = function( deltaT_s, drawingContext) {
if (this.timeType == 'system') {
this.time_s = (window.performance.now() - this.birthTime)/1000.0;
} else {
this.time_s += deltaT_s;
}
if ((this.message != "") && (this.time_s < this.timeLimit_s)) {
// Split each message into multiple lines.
// Then, split each line on the formatting [code] pattern.
// e.g. this message has two lines. \\In this second line, the word [25px Arial,red]special[base] is highlighted by larger font and red color.
// e.g. the word [base,pink]special[base] has no font size change and a color change...
// e.g. the word [30px Arial]special[base] has a font size change and no color change...
// e.g. these [20px Arial]words [25px Arial]have [30px Arial]larger fonts [base]and then go back to normal...
var lines = this.message.split("\\");
// regular expression for finding a formatting code surrounded by brackets
var formatPattern = /(\[.*?\])/;
for (var line_index in lines) {
var formatZones = lines[ line_index].split( formatPattern);
var x_px = this.loc_px.x;
var zoneFont = this.font;
var zoneColor = this.color;
for (var zone_index in formatZones) {
// Check each zone for the [code] pattern and determine the code without the surrounding brackets.
var formatCodeMatch = formatZones[ zone_index].match( formatPattern);
if (formatCodeMatch) {
var formatCode = formatCodeMatch[0].slice(1,-1); // strip off the brackets
if (formatCode == "base") {
zoneFont = this.font;
zoneColor = this.color;
} else {
// check if color is specified
var formatParts = formatCode.split(",");
if (formatParts.length == 2) {
var zoneFont = (formatParts[0] == "base") ? this.font : formatParts[0];
var zoneColor = (formatParts[1] == "base") ? this.color : formatParts[1];
} else {
var zoneFont = formatCode;
var zoneColor = this.color;
}
}
} else {
drawingContext.font = zoneFont;
drawingContext.fillStyle = zoneColor;
var y_px = this.loc_px.y + (line_index * this.lineHeight_px);
drawingContext.textAlign = "left";
drawingContext.fillText( formatZones[ zone_index], x_px, y_px);
// Move the position pointer to the right after rendering the zone string.
x_px += drawingContext.measureText( formatZones[ zone_index]).width;
}
}
}
} else {
// Before ending the message, make an optional pop sound.
if (this.popAtEnd && (this.message != "")) gW.sounds['lowPop'].play();
this.message = "";
this.time_s = 0;
// If it's a series, check to see if there's another message...
if (this.messageSeries) {
this.index += 1;
if (this.messageSeries[this.index]) {
// Update the characteristics of the text if changes have been supplied in the series.
if (this.messageSeries[this.index].loc_px) this.loc_px = this.messageSeries[this.index].loc_px;
if (this.messageSeries[this.index].font) this.font = this.messageSeries[this.index].font;
this.popAtEnd = setDefault( this.messageSeries[this.index].popAtEnd, false);
this.message = this.messageSeries[this.index].message;
this.timeLimit_s = this.messageSeries[this.index].tL_s;
this.time_s = 0;
this.birthTime = window.performance.now();
}
}
}
}
function MultiSelect() {
DrawingFunctions.call(this); // Inherit attributes
// map of pucks and walls in the multiselect as keyed by name
this.map = {};
this.center_2d_m = new Vec2D(0,0);
this.findCenterEnabled = true;
// the select (and delete mode) that is presented via the tab-key menu for multiselect
this.selectMode = ['normal','everything','springs','revolute joints'];
this.selectModeIndex = 0;
this.selectModeMessage = ['normal ([base]springs & revolute joints, then pucks)', 'everything [base]in multi-select', 'springs [base]only', 'revolute joints [base]only'];
// via the enter-key stepper, an array containing the names of springs or revolute joints that are connected to pucks/walls in this.map
// Stepping through this list, draws focus to a particular spring or revolute joint.
this.connectedNames = [];
this.focusIndex = 0;
this.candidateReportPasteDelete = null;
}
MultiSelect.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
MultiSelect.prototype.constructor = MultiSelect; // Rename the constructor (after inheriting)
// A method that loops over the selected objects (this.map) of this instance of MultiSelect
MultiSelect.prototype.applyToAll = function( doThis) {
for (var objName in this.map) {
var tableObj = this.map[ objName];
doThis( tableObj);
}
}
MultiSelect.prototype.resetAll = function() {
Spring.applyToAll( spring => { spring.selected = false; } );
Joint.applyToAll( joint => { joint.selected = false; } );
this.applyToAll( msObject => {
msObject.selectionPoint_l_2d_m = new Vec2D(0,0);
});
this.map = {};
this.resetCenter();
}
MultiSelect.prototype.resetCenter = function() {
this.center_2d_m = new Vec2D(0,0);
this.findCenterEnabled = true;
}
MultiSelect.prototype.count = function() {
return Object.keys(this.map).length;
}
MultiSelect.prototype.findCenter = function() {
this.center_2d_m = new Vec2D(0,0);
this.applyToAll( tableObj => {
this.center_2d_m = this.center_2d_m.add( tableObj.position_2d_m);
});
this.center_2d_m = this.center_2d_m.scaleBy( 1.0 / this.count());
}
MultiSelect.prototype.drawCenter = function( drawingContext) {
this.findCenter();
let center_2d_px = screenFromWorld( this.center_2d_m);
this.drawCircle( drawingContext, center_2d_px, {'borderColor':'yellow', 'borderWidth_px':1, 'fillColor':'black', 'radius_px':5});
// draw cross hairs
let dx_2d_px = new Vec2D( 8, 0);
let dy_2d_px = new Vec2D( 0, 8);
// horizontal line
this.drawLine( drawingContext, center_2d_px.add( dx_2d_px) , center_2d_px.subtract( dx_2d_px), {'width_px':2, 'color':'yellow'});
// vertical line
this.drawLine( drawingContext, center_2d_px.add( dy_2d_px) , center_2d_px.subtract( dy_2d_px), {'width_px':2, 'color':'yellow'});
}
MultiSelect.prototype.removeOne = function( theBody) {
// un-select the springs
Spring.findAll_InMultiSelect( spring => spring.selected = false);
Joint.findAll_InMultiSelect( joint => joint.selected = false);
delete this.map[ theBody.name];
// re-select the springs based on the updated map
Spring.findAll_InMultiSelect( spring => spring.selected = true);
Joint.findAll_InMultiSelect( joint => joint.selected = true);
}
MultiSelect.prototype.pasteCopyAtCursor = function() {
if (this.count() < 1) {
gW.messages['help'].newMessage("Nothing in multi-select. Use shift (or alt) key to multi-select.", 1.0);
return;
}
this.findCenter();
// Offset between the center of the group and the cursor position.
var changeInPosition_2d_m = gW.clients['local'].mouse_2d_m.subtract( this.center_2d_m);
// A temporary map to associated the original pucks to the copies.
var copyMap = {};
// Copy pucks, pins, and walls to the cursor position.
this.applyToAll( tableObj => {
// Exclude navigation pins and client pucks.
if ( ! (tableObj.nextPinName || tableObj.clientName) ) {
var newPosition_2d_m = tableObj.position_2d_m.add( changeInPosition_2d_m);
var newTableObj = tableObj.copyThisOne({'position_2d_m':newPosition_2d_m});
copyMap[tableObj.name] = newTableObj;
} else {
gW.messages['help'].newMessage("Note: client pucks and navigation pins are excluded\\ from multi-select replication.", 2.0);
}
});
// Copy all the springs onto the newly created pucks. Use the copyMap to determine
// correspondence.
Spring.findAll_InMultiSelect( spring => {
// Exclude navigation springs
if ( ! (spring.navigationForNPC)) {
// Copy this spring onto these two pucks.
var targetPuck1 = copyMap[ spring.spo1.name];
var targetPuck2 = copyMap[ spring.spo2.name];
spring.copyThisOne( targetPuck1, targetPuck2);
}
});
Joint.findAll_InMultiSelect( joint => {
// Copy this joint onto these two table objects.
var tableObj1 = copyMap[ joint.jto1.name];
var tableObj2 = copyMap[ joint.jto2.name];
joint.copyThisOne( tableObj1, tableObj2);
});
}
MultiSelect.prototype.arc = function( placement_2d_m) {
console.log("inside arc");
// half circle
let n_rotations = this.count() - 2; // change to 1 for a true half circle
let angle_delta_r = (Math.PI)/n_rotations;
let angle_delta_deg = 180/n_rotations;
let maxRadius_m = 0;
let allCircularPucks = true;
this.applyToAll( msObject => {
if (msObject.radius_m > maxRadius_m) maxRadius_m = msObject.radius_m;
if ((msObject.constructor.name != "Puck") || (msObject.shape != "circle")) allCircularPucks = false;
});
if (( ! allCircularPucks) || (this.count() < 9)) {
gW.messages['help'].newMessage('Must select at least 9 pucks, all circular.', 3.0);
return;
}
let gap_m = 0.10 * maxRadius_m; // 0.05
/*
Spoke length is the distance from the center of the group out to the center of one puck such
that n pucks line up on a half circle. It might help to imagine the circular pucks replaced
by line segments (a diameter).
tan( angle_delta_r / 2) = opp/adj = maxRadius_m / spoke_length
This calculation is more properly used with the chain loop and springy-chain loop calculations.
In those cases the neighboring pucks don't collide. Here, circular pucks may touch at points
off the diameter, as their diameters are placed on the alignment circle. This is more of an issue
as the number of pucks gets smaller, avoided by insisting on a count of 9 or more.
*/
let spokeLength_m = (maxRadius_m + gap_m) / Math.tan( angle_delta_r/2.0);
let spoke_2d_m = new cP.Vec2D( spokeLength_m, 0.0);
this.applyToAll( msObject => {
let newPuckPosition_2d_m = placement_2d_m.add( spoke_2d_m);
msObject.setPosition( newPuckPosition_2d_m, 0);
spoke_2d_m.rotated_by( angle_delta_deg);
});
}
MultiSelect.prototype.align = function() {
// Align the selected objects between the two outermost (the two most separated) objects and linearize the loss related (e.g. friction and drag) attributes.
// Need at least 3. Otherwise, warn then bail.
let n_selected = this.count();
if (n_selected < 3) {
gW.messages['help'].newMessage('Select at least three objects for alignment. \\ The outermost two define the endpoints of the line. \\ Alternately, use alt-l to run the alignment.', 5.0);
return;
}
/*
Tried this (commented) approach first, but it failed (and sometimes froze the browser tab) to pick two outliers if they were not distinctive.
For example, a group of objects equally spaced around the parameter of a circle.
// Calculate each object's distance from the center of the group.
this.findCenter();
let distances = [];
this.applyToAll( msObject => {
let distanceFromCenter_m = this.center_2d_m.subtract( msObject.position_2d_m).length();
distances.push({'name':msObject.name , 'distance_m':distanceFromCenter_m});
});
// Find the two outermost objects by sorting by distance, descending. Use the first two in the sorted list.
distances.sort(function (a, b) {
return b.distance_m - a.distance_m;
});
*/
// Find the most separated pair of objects (the outer two) by considering every possible combination.
let namesInMultiSelectMap = Object.keys(this.map);
let max_separation_m = 0;
let j_max = null;
let k_max = null;
for (let j = 0, len = namesInMultiSelectMap.length; j < len; j++) {
for (let k = j+1; k < len; k++) {
let pos_j_2d_m = this.map[ namesInMultiSelectMap[j]].position_2d_m;
let pos_k_2d_m = this.map[ namesInMultiSelectMap[k]].position_2d_m;
let separation_m = pos_j_2d_m.subtract( pos_k_2d_m).length();
if (separation_m > max_separation_m) {
max_separation_m = separation_m;
j_max = j;
k_max = k;
}
}
}
let outer_A = this.map[ namesInMultiSelectMap[ j_max] ];
let outer_A_2d_m = outer_A.position_2d_m;
let outer_B = this.map[ namesInMultiSelectMap[ k_max] ];
let outer_B_2d_m = outer_B.position_2d_m;
// Create an equally spaced set of positions between these two objects.
let line_positions_by_index = {};
let line_position_index_by_name = {};
let incremental_2d_m = outer_B_2d_m.subtract( outer_A_2d_m).scaleBy( 1 / (n_selected-1) );
for (let i = 1; i < n_selected-1 ; i++) {
let positionOnLine_2d_m = outer_A_2d_m.add( incremental_2d_m.scaleBy( i ) );
line_positions_by_index[ i] = {};
line_positions_by_index[ i].p_2d_m = positionOnLine_2d_m;
line_positions_by_index[ i].used = false;
}
// Move each of the objects (except the outer two) to one of the line positions.
this.applyToAll( msObject => {
if ((msObject.name != namesInMultiSelectMap[ j_max]) && (msObject.name != namesInMultiSelectMap[ k_max])) {
// Find the closest line position that hasn't already been taken.
let d_to_candidate_min_m = 100;
let k_min = null;
for (let k in line_positions_by_index) {
if ( ! line_positions_by_index[ k].used) {
let d_to_candidate_m = msObject.position_2d_m.subtract( line_positions_by_index[ k].p_2d_m).length();
if (d_to_candidate_m < d_to_candidate_min_m) {
d_to_candidate_min_m = d_to_candidate_m;
k_min = k;
}
}
}
msObject.position_2d_m = line_positions_by_index[ k_min].p_2d_m;
msObject.b2d.SetPosition( msObject.position_2d_m);
line_position_index_by_name[ msObject.name] = k_min;
line_positions_by_index[ k_min].used = true;
}
});
// Check for deltas in the attributes of the two outer objects, A and B.
let attributes = { 'restitution':{'delta':null}, 'friction':{'delta':null}, 'linDamp':{'delta':null}, 'angDamp':{'delta':null} };
for (let attributeName in attributes) {
if (outer_A[ attributeName] != outer_B[ attributeName]) {
attributes[ attributeName].delta = outer_B[ attributeName] - outer_A[ attributeName];
}
}
this.applyToAll( msObject => {
// Linearize the attributes (that have a delta) based on line position.
let helpString_names = ""
for (let attributeName in attributes) {
if (attributes[ attributeName].delta) {
helpString_names += attributeName + ", ";
let line_position_index = line_position_index_by_name[ msObject.name];
if (line_position_index) {
msObject[ attributeName] = outer_A[ attributeName] + line_position_index * (attributes[ attributeName].delta /(n_selected-1));
}
}
}
if (helpString_names != "") {
gW.messages['help'].newMessage( helpString_names.slice(0,-2) + " linearized", 1.5);
}
// Update the box2d fixture attributes
msObject.b2d.m_fixtureList.SetRestitution( msObject.restitution);
msObject.b2d.m_fixtureList.SetFriction( msObject.friction);
// Update the box2d body attributes
msObject.b2d.SetLinearDamping( msObject.linDamp);
msObject.b2d.SetAngularDamping( msObject.angDamp);
// Inhibit changes associated with the gravity toggle.
msObject.restitution_fixed = true;
msObject.friction_fixed = true;
});
}
MultiSelect.prototype.resetStepper = function() {
this.connectedNames = [];
this.focusIndex = -1;
this.candidateReportPasteDelete = null;
}
MultiSelect.prototype.stepThroughArray = function( map) {
if (this.connectedNames.length > 0) {
if (gW.clients['local'].key_shift != 'D') {
if (this.focusIndex < (this.connectedNames.length - 1)) {
this.focusIndex++;
} else {
this.focusIndex = 0;
}
} else {
if (this.focusIndex > 0) {
this.focusIndex--;
} else {
this.focusIndex = this.connectedNames.length - 1;
}
}
this.candidateReportPasteDelete = this.connectedNames[ this.focusIndex];
map[ this.candidateReportPasteDelete].selected = false;
//console.log('enter key, ' + this.focusIndex + ", " + this.connectedNames[ this.focusIndex]);
}
}
MultiSelect.prototype.deleteCandidate = function( map) {
map[ this.candidateReportPasteDelete].deleteThisOne({});
this.connectedNames = this.connectedNames.filter( eachName => (eachName != this.candidateReportPasteDelete) );
this.candidateReportPasteDelete = null;
this.focusIndex -= 1;
if (this.focusIndex < 0) this.focusIndex = 0;
}
function SelectBox( pars) {
DrawingFunctions.call(this); // Inherit attributes
this.clickPoint_2d_px = setDefault( pars.clickPoint_2d_px, new Vec2D(0,0));
this.currentMouse_2d_px = setDefault( pars.currentMouse_2d_px, new Vec2D(0,0));
this.enabled = false;
this.limits = {};
}
// Make this a module-level function, not part of the prototype, so it can be used in the callback of the QueryAABB.
// Check if this point is inside the bounding limits of the box.
SelectBox.pointInside = function( p_2d_m, limits) {
if (( p_2d_m.x > limits.min_x ) && ( p_2d_m.x < limits.max_x ) && ( p_2d_m.y > limits.min_y ) && ( p_2d_m.y < limits.max_y )) {
return true;
} else {
return false;
}
}
SelectBox.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
SelectBox.prototype.constructor = SelectBox; // Rename the constructor (after inheriting)
SelectBox.prototype.selectBodiesInBox = function() {
var aabb = new b2AABB();
// The two corners of the box, 1 and 2, in world coordinates.
var c1_2d_m = worldFromScreen( this.clickPoint_2d_px);
var c2_2d_m = worldFromScreen( this.currentMouse_2d_px);
this.limits.min_x = Math.min(c1_2d_m.x, c2_2d_m.x);
this.limits.max_x = Math.max(c1_2d_m.x, c2_2d_m.x);
this.limits.min_y = Math.min(c1_2d_m.y, c2_2d_m.y);
this.limits.max_y = Math.max(c1_2d_m.y, c2_2d_m.y);
// Provide the corners with the lowest values (lower left) and the highest values (upper right)
aabb.lowerBound.Set( this.limits.min_x, this.limits.min_y);
aabb.upperBound.Set( this.limits.max_x, this.limits.max_y);
// Query the world for overlapping shapes.
var objectCount = 0;
// The callback function can't use "this" so make a reference in the local scope.
var limits = this.limits;
// This runs the box query. The function gets called once for each fixture found
// to be overlapping the box.
gW.world.QueryAABB( function( fixture) {
var bd2_Body = fixture.GetBody();
var table_body = gW.tableMap.get( bd2_Body);
// COM of this body.
var p_2d_m = table_body.position_2d_m;
// Check if Center-Of-Mass of this object is within the selection box. This is needed because the
// query returns all bodies for which their bounding box is overlapping the selection box. So this
// give more selection control to avoid nearby objects.
var itsInside = SelectBox.pointInside( p_2d_m, limits);
// Don't select walls or pins if the editor is off.
if (itsInside && !(!gW.dC.editor.checked && ((table_body.constructor.name == "Wall") || (table_body.constructor.name == "Pin")))) {
objectCount += 1;
// Add this body to the hostMSelect map.
gW.hostMSelect.map[ table_body.name] = table_body;
}
// Keep looking at all the fixtures found in the query.
return true;
}, aabb);
/*
Check each point in hostMSelect map. Remove any that are no longer in the box.
Wrote this in three different ways below: (1) with a loop over the map, (2) passing
a function to the applyToAll method, and (3) binding the function to the hostMSelect
object (setting "this") then passing it to applyToAll. The 3rd one is being used.
for (var objName in gW.hostMSelect.map) {
var tableObj = gW.hostMSelect.map[ objName];
if ( ! SelectBox.pointInside(tableObj.position_2d_m, this.limits)) {
gW.hostMSelect.removeOne( tableObj);
}
}
or
gW.hostMSelect.applyToAll( function( tableObj) {
if ( ! SelectBox.pointInside(tableObj.position_2d_m, limits)) {
gW.hostMSelect.removeOne( tableObj);
};
});
or
Note "limits" is defined in the surrounding scope here. The "this"
reference points to the gW.hostMSelect object as dictated in the call
to bind method of the function that's being passed in.
gW.hostMSelect.applyToAll( function( tableObj) {
if ( ! SelectBox.pointInside(tableObj.position_2d_m, limits)) {
this.removeOne( tableObj);
};
}.bind( gW.hostMSelect));
or
Using arrow-function notation. And without using bind and the "this" to get at the removeone method.
Note you can't (and shouldn't want to) bind to an arrow function. Must use a regular function (see above).
Generally the arrow functions are nice for passing in a function so that the "this" in the function
refers to the surrounding context. Of course, can't use "this", and the surrounding context here, to
get at removeOne, since it is part of the MultiSelect class.
*/
gW.hostMSelect.applyToAll( tableObj => {
if (!SelectBox.pointInside( tableObj.position_2d_m, limits)) gW.hostMSelect.removeOne( tableObj);
});
}
SelectBox.prototype.start = function() {
Puck.applyToAll( puck => puck.selectionPoint_l_2d_m = new Vec2D(0,0) );
this.enabled = true;
this.clickPoint_2d_px = gW.clients['local'].mouse_2d_px;
}
SelectBox.prototype.stop = function() {
this.enabled = false;
}
SelectBox.prototype.update = function() {
this.currentMouse_2d_px = gW.clients['local'].mouse_2d_px;
this.selectBodiesInBox();
}
SelectBox.prototype.draw = function( drawingContext) {
var corners_2d_px = [this.clickPoint_2d_px, new Vec2D(this.currentMouse_2d_px.x, this.clickPoint_2d_px.y),
this.currentMouse_2d_px, new Vec2D(this.clickPoint_2d_px.x, this.currentMouse_2d_px.y)];
this.drawPolygon( drawingContext, corners_2d_px, {'borderColor':'red', 'fillIt':false});
}
function Client( pars) {
DrawingFunctions.call(this); // inherit
this.parsAtBirth = pars;
//this.alsoThese = [];
this.color = setDefault( pars.color, "red");
// Used for remembering the host's server name, for everyone but the host (who's this.name is 'local'),
// this parameter is equal to this.name.
this.nameFromServer = setDefault( pars.nameFromServer, null);
// Incrementing the network client name is done in server.js.
this.name = setDefault( pars.name, "manWithNoName");
// Increment the NPC index, but use the higher value.
if (this.name.slice(0,3) == 'NPC') {
Client.npcIndex += 1;
Client.npcIndex = Math.max(Client.npcIndex, Number(this.name.slice(3)));
this.name = 'NPC' + Client.npcIndex;
}
// Add this client to the map.
gW.clients[this.name] = this;
this.puck = null;
this.player = setDefault( pars.player, true);
this.nickName = setDefault( pars.nickName, null);
this.winCount = 0; // use this to suggest a nickname after two game wins
this.virtualGamePadUsage = false; // usage in a game
this.twoThumbsEnabled = false; // client in tt fullscreen mode
this.touchScreenUsage = false;
this.deviceType = 'desktop';
this.mouseDown = false;
this.mouseUsage = false;
this.button = null;
this.raw_2d_px = null; // used only for the 'local' (host) in menu operations for gW.tableActions
// Initially put the drawn cursor (for the local user) out of range of the canvas. That way the cursor doesn't
// render there initially if the page is refreshed, looks cleaner when first coming to the page.
if (this.name == 'local') {
this.mouse_2d_px = new Vec2D(-20, -20);
} else {
this.mouse_2d_px = new Vec2D(-10, -10);
}
this.mouse_2d_m = worldFromScreen( this.mouse_2d_px);
this.mouse_async_2d_px = this.mouse_2d_m;
// this one is used in calculating cursor speed...
this.prev_mouse_2d_px = this.mouse_2d_px;
// used with fine moves...
this.prevNormalCursorPosOnCanvas_2d_px = this.mouse_2d_px;
this.previousFine_2d_px = new Vec2D(400, 300);
this.fineMovesState = 'off';
this.fMT = {'count':0}; //fine Move Transition
// Make a cursor pin for all human clients.
if (this.name.slice(0,3) != 'NPC') {
this.pin = new Pin( Object.assign({}, this.mouse_2d_m), {'name':this.name, 'cursorPin':true, 'borderColor':'white', 'fillColor':this.color});
} else {
this.pin = null;
}
// ghost ball state
this.gBS = {};
gB.resetPathAfterShot( this);
// box2d cursor sensor for pool shots
this.gBS.readyToDraw = false;
this.b2dSensor = null;
this.sensorTargetName = null;
this.sensorContact = null;
// method to indicate these two keys are down without actually holding them down.
this.ctrlShiftLock = setDefault( pars.ctrlShiftLock, false);
// pool-game shot-speed lock
this.poolShotLocked = setDefault( pars.poolShotLocked, false);
this.poolShotLockedSpeed_mps = setDefault( pars.poolShotLockedSpeed_mps, 0);
this.poolShotCount = 0;
this.pocketedBallCount = 0;
// more ghost-pool related parameters...
this.stripesOrSolids = "table is open";
this.scratchedDuringGame = false;
this.objectBallFoulDuringGame = false;
this.safetyShotFoulDuringGame = false;
this.mouseStopMessage = "";
this.mouseStopPenalty = 0;
this.mouseStopFoulDuringGame = false;
this.selectedBody = null;
// Selection point (in the local coordinate system of the selected object).
this.selectionPoint_l_2d_m = null;
this.cursorSpring = null;
// Initialize all the key values to be Up.
for (var key in gW.keyMap) this[gW.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 (not each
frame). This technique is needed in cases where action is potentially
triggered each frame and it is not possible to compare the new key state
(coming from a client or the local keyboard) with the current key state.
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.
The action repeats as the key state is inspected each frame (and seen to
be down).
Note there is an area in this code where pure-local-client key events
are handled to avoid repetition; see the keydown area in this file.
There, repetition is caused by holding the key down and the associated
repeated firing of the keydown event. There, new and current states can
be compared to avoid repetition.
See also the updateClientState function and how it suppressed
unwanted repetition by comparing new and current states.
*/
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)];
this.NPC_guncooling_timer_s = 0.0;
this.NPC_guncooling_timer_limit_s = 2.0;
this.NPC_shield_timer_s = 0.0;
this.NPC_shield_timer_limit_s = 0.5;
this.NPC_pin_timer_s = setDefault( pars.NPC_pin_timer_s, 0.0);
this.NPC_pin_timer_limit_s = setDefault( pars.NPC_pin_timer_limit_s, 5.0);
this.NPC_aimStepCount = 0;
this.NPC_aimStepCount_limit = 20;
this.NPC_skipFrame = false;
this.gunAngle_timer_s = 0.0;
this.gunAngle_timer_limit_s = 0.03;
this.bulletAgeLimit_ms = setDefault( pars.bulletAgeLimit_ms, null);
// rtc contains WebRTC peer connection and data channel objects.
this.rtc = new hC.RTC({});
// Score for the leaderboard
this.score = 0;
// for drawing a little rectangle that helps to sync client and host video captures
this.sendDrawSyncCommand = null;
}
// Variables common to all instances of Client...
Client.npcIndex = 0;
/*
The drag_c parameter affects a drag force that depends on the
absolute motion of the COM of the puck (not relative to cursor). This is
needed for providing the user with a controlled selection (and
positioning) of pucks. If you set these to zero you'll see it's just too
bouncy (and too much orbital motion). Unfortunately, this also gives a
somewhat counterintuitive feel when selecting long rectangular pucks,
near an end edge, with gravity on (the expected swing is strongly
damped). Refer to Spring.prototype.force_on_pucks to see where these
drag forces are applied. There is also the usual relative-motion drag
parameter (spring damper), damper_Ns2pm2, that is set to the default
value (0.5) here. I found this value is less useful here because the
drag_c is being used.
*/
Client.mouse_springs = {'0':{'strength_Npm': 60.0, 'damper_Ns2pm2': 0.5, 'drag_c': 60.0/30.0, 'unstretched_width_m': 0.060}, // 'drag_c': 2.0
'1':{'strength_Npm': 2.0, 'damper_Ns2pm2': 0.5, 'drag_c': 2.0/30.0, 'unstretched_width_m': 0.002}, // 0.1
'2':{'strength_Npm': 1000.0, 'damper_Ns2pm2': 0.5, 'drag_c': 1000.0/30.0, 'unstretched_width_m': 0.100}}; // 20.0
Client.colors = {'1':'yellow','2':'cyan','3':'green','4':'pink','5':'orange',
'6':'brown','7':'greenyellow','8':'blue','9':'tan','0':'purple'};
Client.lightColors = ['yellow', 'greenyellow', 'pink', 'cyan', 'tan'];
Client.applyToAll = function ( doThis) {
for (var clientName in gW.clients) {
var client = gW.clients[ clientName];
doThis( client);
}
}
Client.deleteNPCs = function() {
Client.applyToAll( client => {if (client.name.slice(0,3) == 'NPC') delete gW.clients[ client.name]});
Client.npcIndex = 0;
}
Client.startingPandV = [];
Client.scoreSummary = [];
Client.winnerBonusGiven = false;
Client.resetScores = function() {
Client.applyToAll( client => {
client.mouseUsage = false;
client.touchScreenUsage = false;
client.virtualGamePadUsage = false;
client.score = 0;
});
// If the npc are still paused, indicate pause usage.
if (pP.getNpcSleep()) {
pP.setNpcSleepUsage( true);
} else {
pP.setNpcSleepUsage( false);
}
pP.setPuckPopperTimer_s(0);
Client.winnerBonusGiven = false;
Client.scoreSummary = [];
}
// Sometimes it's just better to see 'host' displayed instead of 'local'.
Client.translateIfLocal = function( clientName) {
var nameString;
if (clientName == 'local') {
nameString = 'host';
} else {
nameString = clientName;
}
return nameString;
}
Client.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
Client.prototype.constructor = Client; // Rename the constructor (after inheriting)
Client.prototype.nameString = function( nickNameOnly=false) {
var nameString, finalNameString;
nameString = Client.translateIfLocal( this.name);
if (this.nickName) {
if ( ! nickNameOnly) {
finalNameString = this.nickName + ' (' + nameString + ')';
} else {
finalNameString = this.nickName;
}
} else {
finalNameString = nameString;
}
return finalNameString;
}
Client.prototype.addScoreToSummary = function( winnerTimeString, demoIndex, npcSleepUsage) {
var finalNameString, mouseString, npcSleepString, virtualGamePadString;
finalNameString = this.nameString();
// Clear the mouseString warning for Jello Madness. Mouse is always used.
if ( [4,5,6].includes( demoIndex) ) {
mouseString = '';
} else {
mouseString = (this.mouseUsage) ? 'x':'';
}
npcSleepString = (npcSleepUsage) ? 'x':'';
virtualGamePadString = (this.virtualGamePadUsage) ? 'x':'';
// The randomIndex provides a way to nearly uniquely associate records in the leaderboard report with the local game summary.
Client.scoreSummary.push( {'score':this.score, 'rawName':this.name, 'name':finalNameString, 'virtualGamePad':virtualGamePadString,
'winner':winnerTimeString, 'mouse':mouseString, 'npcSleep':npcSleepString, 'randomIndex':Math.floor((Math.random() * 100000))} );
}
Client.prototype.createBox2dSensor = function( radius_m) {
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_dynamicBody; // same type as a puck
bodyDef.allowSleep = false;
this.b2dSensor = gW.world.CreateBody( bodyDef);
//this.b2dSensor.SetAwake(true);
var fixDef = new b2FixtureDef;
fixDef.shape = new b2CircleShape( radius_m);
// Turned out to be better (offer more control) to use box2d contact events to inhibit the collision response.
// (see contactNormals in ghostBall.js)
fixDef.isSensor = false;
this.b2dSensor.CreateFixture( fixDef);
// Set the initial position of the sensor
this.b2dSensor.SetPosition( this.mouse_2d_m);
// Mark this....
this.b2dSensor.SetUserData('ghost-sensor');
// Use the table map to get back to this client from the b2d event handler.
gW.tableMap.set(this.b2dSensor, this);
}
Client.prototype.updateBox2dSensor = function( displace_2d_m) {
this.b2dSensor.SetPosition( this.mouse_2d_m.add( displace_2d_m));
}
Client.prototype.deleteBox2dSensor = function() {
gW.tableMap.delete( this.b2dSensor);
gW.world.DestroyBody( this.b2dSensor);
this.sensorTargetName = null;
this.b2dSensor = null;
}
Client.prototype.checkForMouseSelection = function() {
// Process selection attempts and object manipulations.
if ((this.selectedBody === null) && (this.mouseDown)) {
// Check for a body at the mouse position.
var selected_b2d_Body = gW.b2d_getBodyAt( this.mouse_2d_m);
if (selected_b2d_Body) {
var selectedBody = gW.tableMap.get( selected_b2d_Body);
if (gW.getDemoVersion().slice(0,3) == "3.d") gB.checkForMouseStops( this, selectedBody);
// Tip for editing walls and pins
if ( (gW.dC.editor.checked) && (this.key_ctrl == "U") && ((selectedBody.constructor.name == "Wall") || (selectedBody.constructor.name == "Pin")) ) {
gW.messages['help'].newMessage('Hold down "ctrl" key to drag walls and pins (host only).', 1.5);
}
// Block the selection on static bodies (walls and pins) by a network client.
if ( ((selectedBody.constructor.name != "Puck") && (this.name != 'local')) ||
// Block wall and pin selection if the wall/pin editor is off.
(!gW.dC.editor.checked && ((selectedBody.constructor.name == "Wall") || (selectedBody.constructor.name == "Pin"))) ) {
selected_b2d_Body = null;
} else {
// Consider the case where client is trying to edit multiple objects (only shift key is down).
if ((this.key_shift == "D") && (this.key_ctrl == "U") && (this.key_alt == "U")) {
// Add this body to the multiple-select map (if not already there).
if (!(selectedBody.name in gW.hostMSelect.map) && (this.button == 0)) {
// Record the local selection point on the body.
if (gW.dC.comSelection.checked) {
selectedBody.selectionPoint_l_2d_m = new Vec2D(0,0);
} else {
selectedBody.selectionPoint_l_2d_m = Vec2D_from_b2Vec2( selected_b2d_Body.GetLocalPoint( this.mouse_2d_m));
}
gW.hostMSelect.map[ selectedBody.name] = selectedBody;
// Remove this body from the map if doing a right-button (2) mouse click.
} else if ((selectedBody.name in gW.hostMSelect.map) && (this.button == 2)) {
gW.hostMSelect.removeOne( selectedBody);
}
// If using the box-selection feature...
} else if ((this.name == 'local') && (this.key_alt == "D") && (this.key_ctrl == "U")) {
if ((selectedBody.name in gW.hostMSelect.map) && (this.button == 2)) {
gW.hostMSelect.removeOne( selectedBody);
}
// Normal single-body selection:
// Allow single-body pin selection only if the wall/pin editor is on.
} else if ( ! ( !gW.dC.editor.checked && (selectedBody.constructor.name == "Pin"))) {
// Which body object has been selected?
this.selectedBody = gW.tableMap.get( selected_b2d_Body);
if (this.selectedBody.clientName) {
var clientNameString = '(' + this.selectedBody.clientName + ')';
} else {
var clientNameString = '';
}
// Mark it as selected and record the local point.
this.selectionPoint_l_2d_m = Vec2D_from_b2Vec2( selected_b2d_Body.GetLocalPoint( this.mouse_2d_m));
this.modifyCursorSpring('attach');
// If selecting a small puck with right-button on mouse, warn user about stability:
if ((this.selectedBody.mass_kg < 0.15) && (this.button == 2)) {
gW.messages['help'].newMessage("For a small puck, use the middle or left mouse button.", 3.0);
}
// If using the control key (deterministic drag or rotation) and there already are
// some bodies in the multi-select, add this body to the multi-select group. This
// insures normal group-rotation behaviors.
if ((this.key_ctrl == "D") && (gW.hostMSelect.count() > 0)) {
gW.hostMSelect.map[ selectedBody.name] = selectedBody;
}
}
}
}
// The mouse button has been released
} else if ((this.selectedBody) && ( ! this.mouseDown)) {
// Shoot the (single-selected) puck with the cursor spring energy.
//if ((this.key_ctrl == 'D') && (this.key_shift == 'D') && (this.cursorSpring)) {
// this.poolShot();
// gW.messages['help'].resetMessage(); // stop the help pool players
//}
//this.modifyCursorSpring('dettach');
}
}
Client.prototype.modifyCursorSpring = function( mode) {
// If there isn't already a cursor spring, add one.
if ((mode == 'attach') && ( ! this.cursorSpring)) {
// Local selection point on puck.
if ( ( ! gW.dC.comSelection.checked) || (this.key_ctrl == "D") || (this.ctrlShiftLock && (this.selectedBody.shape != 'circle')) ) {
// For this special case, make sure the COM control reflects the non-COM action. Without this line,
// you would have to click c twice in the basketball game for any of the images that are based on rectangular pucks.
if (this.ctrlShiftLock && (this.selectedBody.shape != 'circle')) gW.dC.comSelection.checked = false;
var selectionPoint_l_2d_m = this.selectionPoint_l_2d_m.copy();
} else {
var selectionPoint_l_2d_m = new Vec2D(0.0,0.0);
}
/*
Always use a normal spring for the cursor ('softConstraints':false). Have played around with using the distance joints but they
seem to have similar instability problems with small masses and strong springs.
4:20 PM Tue May 12, 2020: Changed my mind. Going to let the cursor spring type be affected by the toggle (shift-s), took out the softConstraints
specification: 'softConstraints':false.
11:52 AM Sun May 22, 2022: ...and two years later I've restricted it again to be fixed (as Hooke's Law) and not allow soft constraints. Noticed that in the basketball game
the ball drifts when aiming if using a distance joint (soft constraint). The (my) Hooke's law spring allows me to better inhibit (shut off) the spring forces when aiming
under the ctrl-shift-locked conditions. Someday, a better solution, that would allow both spring natures in the cursor spring, is to not actually mount the spring
until after the shot is launched (that would be a significant change).
*/
// Note that a cursor spring is created using the client's name (this.name).
this.cursorSpring = new Spring(this.pin, this.selectedBody,
Object.assign({}, Client.mouse_springs[this.button], {'spo2_ap_l_2d_m':selectionPoint_l_2d_m, 'color':this.color, 'forCursor':true, 'name':this.name, 'softConstraints':false}) );
// High drag_c (the default for button 2) was causing instability for right-button manipulation of the inner pucks in a jello grid.
if ((this.selectedBody.jello) && (this.button == 2)) this.cursorSpring.drag_c = 5.0;
if ((this.selectedBody.constructor.name == 'Puck') && ( ! this.compoundBodySelected())) {
this.createBox2dSensor( this.selectedBody.radius_m);
}
} else if ((mode == 'dettach') && (this.cursorSpring)) {
if ((this.selectedBody.constructor.name == 'Puck') && (this.b2dSensor)) {
this.deleteBox2dSensor();
}
this.cursorSpring.deleteThisOne({});
this.cursorSpring = null;
this.selectionPoint_l_2d_m = null;
this.selectedBody = null;
}
}
Client.prototype.moveSBtoPosition = function(theBody, pos_2d_m) {
// move Selected Body to Position
theBody.position_2d_m = pos_2d_m;
theBody.position_2d_px = screenFromWorld( theBody.position_2d_m);
theBody.b2d.SetPosition( pos_2d_m);
// If it's a puck, freeze it, for more predictable put-it-here behavior.
if (theBody.constructor.name == "Puck") {
theBody.velocity_2d_mps = new Vec2D(0.0,0.0);
theBody.b2d.SetLinearVelocity( new Vec2D(0.0,0.0));
theBody.angularSpeed_rps = 0.0;
theBody.b2d.SetAngularVelocity( theBody.angularSpeed_rps);
}
}
Client.prototype.moveToCursorPosition = function() {
// for direct positioning of objects:
// Calculate the world (w) delta between the current mouse position and the original selection point.
// The delta is used for positioning (direct dragging of) bodies so that selection points
// follows the moving mouse location.
var delta_w_2d_m = this.mouse_2d_m.subtract( this.cursorSpring.spo2_ap_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_2d_m = this.selectedBody.position_2d_m.add( delta_w_2d_m);
// Before actually moving it, keep track of the calculated amount of movement.
var movement_2d_m = newPosition_2d_m.subtract( this.selectedBody.position_2d_m);
// Move the single selected body (SB) to the mouse position.
this.moveSBtoPosition( this.selectedBody, newPosition_2d_m);
// Temporarily inhibit the external forces on this puck (this prevents a gradual droop when gravity is on).
if (this.selectedBody.constructor.name == "Puck") this.selectedBody.tempInhibitExtForce = true;
// Move all the other selected bodies by a similar amount.
// Note: the arrow function, used here, will take "this" from the surrounding context.
gW.hostMSelect.applyToAll( tableObj => {
if (tableObj !== this.selectedBody) this.moveSBtoPosition( tableObj, tableObj.position_2d_m.add( movement_2d_m));
// Temporarily inhibit the external forces on this puck (this prevents a gradual droop when gravity is on).
if (tableObj.constructor.name == "Puck") tableObj.tempInhibitExtForce = true;
});
// For this cursor-selected object, if one object or less in multi-select, output its position (for walls and pucks) and elasticity characteristics (for pucks);
if (gW.hostMSelect.count() <= 1) {
let bulletString = ((this.selectedBody.constructor.name == "Puck") && (this.selectedBody.bullet)) ? " (bullet)" : "";
var objReport = "[base,yellow]" + this.selectedBody.name + "[base]" + bulletString +
" @ x:" + this.selectedBody.position_2d_m.x.toFixed(3) + ", " + "y:" + this.selectedBody.position_2d_m.y.toFixed(3) + " m" + "";
//", \u03B8_deg:" + (this.selectedBody.b2d.GetAngle() * 180/Math.PI).toFixed(0);
if ((this.selectedBody.constructor.name == "Puck") || (this.selectedBody.constructor.name == "Wall")) {
if (this.selectedBody.shape == "circle") {
var dimensionsReport = "\\ radius = " + this.selectedBody.radius_m.toFixed(3);
} else {
var dimensionsReport = "\\ width/2, height/2 = " + this.selectedBody.half_width_m.toFixed(3) + ", " + this.selectedBody.half_height_m.toFixed(3);
}
objReport += dimensionsReport;
}
if (this.selectedBody.constructor.name == "Puck") {
let rF = (this.selectedBody.restitution_fixed) ? " (fixed)" : "";
let fF = (this.selectedBody.friction_fixed) ? " (fixed)" : "";
objReport += "\\ restitution = " + this.selectedBody.restitution.toFixed(3) + rF + ", surface friction = " + this.selectedBody.friction.toFixed(3) + fF +
"\\ translational, rotational drag = " + this.selectedBody.linDamp.toFixed(3) + ", " + this.selectedBody.angDamp.toFixed(3);
if (this.selectedBody.imageID) {
let img = document.getElementById( this.selectedBody.imageID);
if (img.title) {
objReport += "\\ title = " + img.title;
}
}
}
gW.messages['help'].newMessage( objReport, 0.05);
}
}
Client.prototype.rotateSB = function(theBody, delta_angle_r) {
if (theBody.constructor.name == "Puck") {
theBody.velocity_2d_mps = new Vec2D(0.0,0.0);
theBody.b2d.SetLinearVelocity( new Vec2D(0.0,0.0));
theBody.angularSpeed_rps = 0.0;
theBody.b2d.SetAngularVelocity( theBody.angularSpeed_rps);
}
// Everything but pins... If you don't exclude pins here, they become un-selectable after
// a rotation with the editor.
if (theBody.constructor.name != "Pin") {
theBody.angle_r += delta_angle_r;
theBody.b2d.SetAngle( theBody.angle_r);
}
}
Client.prototype.rotateToCursorPosition = function() {
var delta_r;
// Rotate about the center of the group.
if (gW.hostMSelect.count() > 1) {
// Find the center only at the beginning of the rotation action.
if (gW.hostMSelect.findCenterEnabled) {
gW.hostMSelect.findCenter();
// Don't do this again until one of the keys is released.
gW.hostMSelect.findCenterEnabled = false;
}
// Measure the rotation relative to the center of the group.
delta_r = gW.hostMSelect.center_2d_m.angleBetweenPoints_r( this.cursorSpring.spo2_ap_w_2d_m, this.mouse_2d_m);
gW.hostMSelect.applyToAll( tableObj => {
// Rotate the vector that runs from the hostMSelect center out to the object center.
var center_to_center_2d = tableObj.position_2d_m.subtract( gW.hostMSelect.center_2d_m);
center_to_center_2d.rotated_by( delta_r * 180.0/ Math.PI );
// Then reassemble the object vector and put the object there.
this.moveSBtoPosition( tableObj, gW.hostMSelect.center_2d_m.add( center_to_center_2d));
// Rotate the object about its center.
this.rotateSB( tableObj, delta_r);
// Temporarily inhibit the forces on this puck (this prevents a gradual droop when gravity is on).
if (tableObj.constructor.name == "Puck") tableObj.tempInhibitExtForce = true;
});
// Rotate about the center of the single object.
} else {
// Find the angle formed by these three points (angle based at the center of this selected body). This is the angle formed
// as the mouse (and cursor pin) moves from the old selection point. Note must use pin position and not simply the mouse
// because the ghost sensor might displace the pin from the cursor.
this.pin.getPosition();
// Rotate, if not attached to the center of the puck...
if ( ! this.cursorSpring.spo2_ap_l_2d_m.zeroLength() ) {
delta_r = this.selectedBody.position_2d_m.angleBetweenPoints_r(this.cursorSpring.spo2_ap_w_2d_m, this.pin.position_2d_m);
} else {
delta_r = 0;
}
this.rotateSB( this.selectedBody, delta_r);
// Temporarily inhibit the forces on this puck (this prevents a gradual droop when gravity is on).
if (this.selectedBody.constructor.name == "Puck") this.selectedBody.tempInhibitExtForce = true;
}
}
Client.prototype.rotateEachAboutItself = function() {
var delta_r = this.selectedBody.position_2d_m.angleBetweenPoints_r(this.cursorSpring.spo2_ap_w_2d_m, this.mouse_2d_m);
if (gW.hostMSelect.count() > 0) {
gW.hostMSelect.applyToAll( tableObj => {
// Don't allow group rotation based on pin selection (avoid wall spinning).
if (this.selectedBody.constructor.name != "Pin") this.rotateSB(tableObj, delta_r);
// Temporarily inhibit the forces on this puck (this prevents a gradual droop when gravity is on).
if (tableObj.constructor.name == "Puck") tableObj.tempInhibitExtForce = true;
});
} else {
this.rotateSB(this.selectedBody, delta_r);
}
}
Client.prototype.drawTriangle = function( drawingContext, position_2d_px, pars) {
// Draw a triangle for the network client's cursor at position_2d_px
var fillIt = setDefault( pars.fillIt, true);
var fillColor = setDefault( pars.fillColor, 'red');
this.triangle_2d_px = [];
var cursorOffset_2d_px = new Vec2D(0,1); //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 = position_2d_px.add(this.triangle_raw_2d_px[i]).add( cursorOffset_2d_px);
// Put it in the triangle array.
this.triangle_2d_px.push( p_2d_px);
}
var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? 'white' : fillColor; // white for color mixing demo
if ( ( ! gW.getPauseErase()) || gW.getLagTesting()) {
this.drawPolygon( drawingContext, this.triangle_2d_px, {'borderColor':'white', 'borderWidth_px':1, 'fillIt':fillIt ,'fillColor':fillColor});
// Use cursor speed to calculate a radius at 2-frames out from the rendered cursor. Useful for quantifying the lag behind the system cursor.
if (gW.getLagTesting() && (this.fineMovesState == 'off')) {
let cursorSpeed_pxps = position_2d_px.subtract( this.prev_mouse_2d_px).length() / gW.getDeltaT_s();
if (cursorSpeed_pxps == 0) gW.aT.cursorSpeed_pxps.reset();
let cursorSpeed_ra_pxps = gW.aT.cursorSpeed_pxps.update( cursorSpeed_pxps);
let radiusFor2FrameLag_px = 2 * gW.getDeltaT_s() * cursorSpeed_ra_pxps;
if ( ! gW.getPauseErase()) this.drawCircle( drawingContext, position_2d_px, {'borderWidth_px':2, 'fillColor':'noFill', 'radius_px':radiusFor2FrameLag_px});
}
}
}
Client.prototype.compoundBodySelected = function() {
return ( Spring.checkIfAttached( this.selectedBody.name) || Joint.checkIfAttached( this.selectedBody.name) );
}
Client.prototype.updateCursor = function() {
this.mouse_2d_m = worldFromScreen( this.mouse_2d_px);
var tryingToShoot_locked = this.ctrlShiftLock && (this.key_ctrl == "U") && (this.key_shift == "U");
var tryingToShoot = ((this.key_ctrl == "D") && (this.key_shift == "D")) || tryingToShoot_locked;
if ( (this.selectedBody) && (this.cursorSpring) && (this.selectedBody.constructor.name == 'Puck') && (gW.hostMSelect.count() == 0) && (tryingToShoot) && ( ! this.compoundBodySelected() ) ) {
gB.updateGhostBall( this);
this.gBS.readyToDraw = true;
} else {
if (this.pin) this.pin.setPosition( this.mouse_2d_m);
}
if (this.cursorSpring) this.cursorSpring.updateEndPoints();
}
Client.prototype.drawCursor = function( drawingContext) {
if (this.mouseDown) {
if ((this.key_shift == "D") || (this.key_alt == "D")) {
this.pin.draw_MultiSelectPoint( drawingContext);
} else {
this.pin.draw( drawingContext, 4); // 4px radius
}
}
// This shows the actual position of the cursor when fineMoves is active.
// This appears as a light-gray outline (no fill) and separate from the normal cursor (drawn next).
if (this.fineMovesState != 'off') {
drawingContext.globalAlpha = 0.70;
this.drawTriangle( drawingContext, this.prevNormalCursorPosOnCanvas_2d_px, {'fillIt':false});
drawingContext.globalAlpha = 1.00;
}
// Normal cursor rendering
this.drawTriangle( drawingContext, this.mouse_2d_px, {'fillColor':this.color});
}
Client.prototype.updateSelectionPoint = function() {
// Calculate (update) the world location of the attachment point for use in force calculations.
this.cursorSpring.spo2_ap_w_2d_m = Vec2D_from_b2Vec2( this.selectedBody.b2d.GetWorldPoint( this.cursorSpring.spo2_ap_l_2d_m));
this.cursorSpring.spo2_ap_w_2d_px = screenFromWorld( this.cursorSpring.spo2_ap_w_2d_m);
}
Client.prototype.drawSelectionPoint = function( drawingContext) {
// Draw small circle at the attachment point.
var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? this.selectedBody.color : this.color;
if ( ! gW.getPauseErase()) {
this.drawCircle( drawingContext, this.cursorSpring.spo2_ap_w_2d_px, {'borderColor':'white', 'borderWidth_px':2, 'fillColor':fillColor, 'radius_px':6}); // 6
}
}
// For use in sound field, demo #2.
function PuckTail( pars) {
DrawingFunctions.call(this); // inherit
this.firstPoint_2d_m = setDefault( pars.firstPoint_2d_m, new Vec2D(1.0, 1.0));
this.initial_radius_m = setDefault( pars.initial_radius_m, 1.0);
// ppf: pixels per frame
this.propSpeed_ppf_px = setDefault( pars.propSpeed_ppf_px, 1);
this.length_limit = setDefault( pars.length_limit, 25);
this.color = setDefault( pars.color, 'lightgray');
this.rainbow = setDefault( pars.rainbow, false);
if (this.rainbow) {
if (pars.hsl) {
this.hsl = new HSLColor( pars.hsl);
} else {
this.hsl = new HSLColor( {'hue':0, 'saturation':80, 'lightness':70, 'steppingKey':'hue', 'stepSize':4} );
}
}
this.machSwitch = setDefault( pars.machSwitch, false);
this.machValue = setDefault( pars.machValue, 0);
// The wait (time in seconds) before making a pure white color ping.
this.markerPingTimerLimit_s = setDefault( pars.markerPingTimerLimit_s, 1.0);
this.markerPingTimer_s = 0.0;
this.values = [];
}
PuckTail.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
PuckTail.prototype.constructor = PuckTail; // Rename the constructor (after inheriting)
PuckTail.prototype.machCalc = function( puckSpeed_mps) {
var waveSpeed_mps = meters_from_px(this.propSpeed_ppf_px) * gW.getFrameRate();
var mach = puckSpeed_mps / waveSpeed_mps;
return mach;
}
PuckTail.prototype.speedFromMach = function() {
var waveSpeed_mps = meters_from_px(this.propSpeed_ppf_px) * gW.getFrameRate();
var puckSpeed_mps = waveSpeed_mps * this.machValue;
return puckSpeed_mps;
}
PuckTail.prototype.update = function( drawingContext, newPoint_2d_m, deltaT_s) {
var lineColor;
if (this.rainbow) {
// hue, saturation, lightness
// 0-360, 0-100%, 0-100%
this.pingColor = this.hsl.colorString();
this.hsl.step();
} else {
this.pingColor = this.color;
}
// Color one ring specially so to see it propagation better.
this.markerPingTimer_s += deltaT_s;
if ((this.markerPingTimer_s > this.markerPingTimerLimit_s) && !this.rainbow) {
this.pingColor = 'white';
this.markerPingTimer_s = 0.0;
}
// This is a reminder that an adjustable emit frequency doesn't render well.
// Best to emit once per frame as is done in the single line that follows.
// Ping out a new ring (once per 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 += this.propSpeed_ppf_px;
// Draw the sound circle (make the 'white' marker ring even more visible, using red, if single stepping).
if (gW.getSingleStep() && (this.values[t].color == 'white')) {
lineColor = 'red'; //#008080 cyan yellow magenta orange
} else {
lineColor = this.values[t].color;
}
this.drawCircle( drawingContext, 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) {
DrawingFunctions.call(this); // Inherit attributes
this.parsAtBirth = pars;
//this.alsoThese = [];
// for removing old bullets
this.ageLimit_ms = setDefault( pars.ageLimit_ms, null);
this.bullet = setDefault( pars.bullet, false);
this.bulletIndication = setDefault( pars.bulletIndication, false);
this.jello = setDefault( pars.jello, false);
this.clientName = setDefault( pars.clientName, null);
if (this.clientName) {
// Don't allow a client puck if there is not already a client. Client first, then puck.
// Throwing an error forces an exit from this constructor.
if (!(gW.clients[this.clientName])) {
var errorObj = new Error('Constructor declines to create a puck for a non-existent client.');
errorObj.name = 'from Puck constructor';
throw errorObj;
}
pP.addToPlayerCount( +1);
if (this.clientName.includes('NPC')) pP.addToNpcCount( +1);
}
if (pars.name) {
this.name = pars.name;
Puck.nameIndex = Math.max(Puck.nameIndex, Number(this.name.slice(4)));
} else {
Puck.nameIndex += 1;
this.name = 'puck' + Puck.nameIndex;
}
this.nameTip_timerLimit_s = 2.0;
this.nameTip_timer_s = 0.0;
this.lowBallFinderCircle_timerLimit_s = 3.0;
this.lowBallFinderCircle_timer_s = this.lowBallFinderCircle_timerLimit_s;
gW.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 = setDefault( pars.color, "DarkSlateGray");
if (this.color.includes('hsl')) {
if (typeof pars.hsl !== "undefined") {
this.hsl = new HSLColor( pars.hsl);
this.color = this.hsl.colorString();
} else {
this.hsl = new HSLColor();
this.hsl.parse( this.color);
}
}
this.borderColor = setDefault( pars.borderColor, "white");
this.shape = setDefault( pars.shape, "circle");
this.imageID = setDefault( pars.imageID, null);
this.imageScale = setDefault( pars.imageScale, 1.0);
this.colorSource = setDefault( pars.colorSource, false);
//this.colorExchange = setDefault( pars.colorExchange, false);
this.drawDuringPE = setDefault( pars.drawDuringPE, true); // PE = pause erase
this.density = setDefault( pars.density, 1.5);
// Linear damping is like a drag force from translational movement through a surrounding fluid.
// Note that springs have the attribute drag_c, with an effect similar to linDamp.
this.linDamp = setDefault( pars.linDamp, 0.0);
// Rotational drag
this.angDamp = setDefault( pars.angDamp, 0.0);
this.hitLimit = setDefault( pars.hitLimit, 10);
this.createTail = setDefault( pars.createTail, false);
this.tail = null;
// www.iforce2d.net/b2dtut/collision-filtering
// For client pucks, assign a negative group index that is based on the puck's name
// This group index can be used to prevent collisions with bullets (having the same negative group index)
// coming from a gun hosted by this puck.
if (this.clientName) {
this.groupIndex = -this.name.slice(4)-1000;
} else {
this.groupIndex = setDefault( pars.groupIndex, 0);
}
// The following are defaults for Box2D.
this.categoryBits = setDefault( pars.categoryBits, 0x0001);
this.maskBits = setDefault( pars.maskBits, 0xFFFF);
// Rotational state
this.angle_r = setDefault( pars.angle_r, 0);
this.angularSpeed_rps = setDefault( pars.angularSpeed_rps, 0);
this.angleLine = setDefault( pars.angleLine, true);
this.borderWidth_px = setDefault( pars.borderWidth_px, 3);
// Put a reference to this puck in the client.
if (this.clientName) {
gW.clients[this.clientName].puck = this;
}
this.age_ms = 0;
//this.createTime = window.performance.now();
// Note that a call to setGravityRelatedParameters() may override the restitution and friction settings
// in what follows unless they have been "fixed" (set to be constant).
// Restitution (elasticity) of the object in collisions
if (typeof pars.restitution === 'undefined') {
if (gW.getG_ON()) {
this.restitution = Puck.restitution_gOn;
} else {
this.restitution = Puck.restitution_gOff;
}
} else {
this.restitution = pars.restitution;
}
// Option to fix restitution to be independent of the g toggle.
this.restitution_fixed = setDefault( pars.restitution_fixed, false);
// Friction (tangential tackiness) of the object in collisions
if (typeof pars.friction === 'undefined') {
if (gW.getG_ON()) {
this.friction = Puck.friction_gOn;
} else {
this.friction = Puck.friction_gOff;
}
} else {
this.friction = pars.friction;
}
// Option to fix friction to be independent of the g toggle.
this.friction_fixed = setDefault( pars.friction_fixed, false);
// Dimensions
this.radius_m = setDefault( pars.radius_m, 1.0);
this.aspectR = setDefault( pars.aspectR, 1.0);
this.half_height_m = setDefault( pars.half_height_m, null);
this.half_width_m = setDefault( 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);
}
if (this.imageID) {
var img = document.getElementById( this.imageID);
let image_aspectRatio = img.width / img.height;
this.half_width_m = this.half_height_m * image_aspectRatio;
this.half_width_px = px_from_meters( this.half_width_m);
}
}
// Tail
if (this.createTail) {
var tailInitialState = {'firstPoint_2d_m':this.position_2d_m, 'initial_radius_m':this.radius_m};
// Add any specified characteristics.
if (pars.tail) {
/*
For pucks that don't have tails, set the creatTail flag to true in a
capture. Then run the capture. That will instantiate the puck with a tail
having editable attributes (in the capture).
*/
let allTailPars = Object.assign({}, tailInitialState, pars.tail);
this.tail = new PuckTail( allTailPars);
} else {
this.tail = new PuckTail( tailInitialState);
}
if (this.tail.machSwitch) {
// Calculate the puck velocity based on the specified Mach number.
var temp_v_2d_mps = new Vec2D(0, this.tail.speedFromMach());
temp_v_2d_mps.matchAngle( this.velocity_2d_mps);
this.velocity_2d_mps = temp_v_2d_mps;
}
}
this.tempInhibitExtForce = false;
this.nonCOM_2d_N = [];
this.sprDamp_force_2d_N = new Vec2D(0.0,0.0);
// This vector is needed for aiming the NPC's navigation jets.
this.navSpringOnly_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
this.gun = null, this.jet = null;
this.rayCastLineLength_m = setDefault( pars.rayCastLineLength_m, 3.5);
this.rayCast_init_deg = setDefault( pars.rayCast_init_deg, 0);
this.rayRotationRate_dps = setDefault( pars.rayRotationRate_dps, 80);
// Disables and hides the jet
this.disableJet = setDefault( pars.disableJet, false);
this.noRecoil = setDefault( pars.noRecoil, false);
this.cannibalize = setDefault( pars.cannibalize, false);
this.bullet_restitution = setDefault( pars.bullet_restitution, 0.92);
// If a bullet puck never hits another puck, this stays false.
this.atLeastOneHit = false;
// It identifies the owner of this (bullet) puck.
// (so you can't shoot yourself in the foot)
this.clientNameOfShooter = setDefault( pars.clientNameOfShooter, null);
this.hitCount = 0;
this.poorHealthFraction = 0;
// Keep track of who shot the most recent bullet to hit this puck.
this.whoShotBullet = null;
this.flash = false;
this.inComing = false;
this.flashCount = 0;
// Navigation spring (not generally the name of any attached spring). There can be only
// one navigation spring.
if (this.clientName && this.clientName.includes('NPC')) {
this.navSpringName = null;
this.pinName = setDefault( pars.pinName, null);
// If there's named pin and it still exists...
if (this.pinName && (gW.aT.pinMap[ this.pinName])) {
this.disableJet = false;
pP.attachNavSpring( this);
} else {
this.disableJet = true;
}
}
// Ghost-pool features
this.spotted = false;
// With network players involved, this choke is used to prevent more than one client from trying to make direct movements (like mouse-drag rotation).
// This is the name of the first client to start a direct-movement on a puck.
this.firstClientDirectMove = null;
// Local selection point where candidate springs are to be attached.
this.selectionPoint_l_2d_m = new Vec2D(0,0);
this.deleted = false;
// All parameters should be set above this point.
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!
gW.tableMap.set(this.b2d, this);
if (this.clientName) {
// Add client controls and give each control a reference to this puck.
this.jet = new pP.Jet(this, {'initial_angle':-20});
this.gun = new pP.Gun(this, {'initial_angle':200, 'indicator':true, 'tube_color':'gray',
'rayCast_init_deg':this.rayCast_init_deg, 'rayRotationRate_dps':this.rayRotationRate_dps, 'rayCastLineLength_m':this.rayCastLineLength_m});
}
this.shield = new pP.Shield(this, {'color':'yellow'});
}
Puck.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
Puck.prototype.constructor = Puck; // Rename the constructor (after inheriting)
Puck.nameIndex = 0;
// Note: these gravity-related defaults are typically set to non-Null values in demoStart (in gwModule).
Puck.restitution_default_gOn = null, Puck.restitution_default_gOff = null;
Puck.restitution_gOn = null, Puck.restitution_gOff = null;
Puck.friction_default_gOn = null, Puck.friction_default_gOff = null;
Puck.friction_gOn = null, Puck.friction_gOff = null;
Puck.g_2d_mps2 = null;
Puck.hostPars = {'radius_m':0.30, 'color':'black', 'colorSource':true, 'clientName':'local', 'hitLimit':20, 'bullet_restitution':0.85, 'linDamp':1.0};
Puck.minRadius_px = 9; // limits cannibalization
Puck.applyToAll = function ( doThis) {
for (var puckName in gW.aT.puckMap) {
var puck = gW.aT.puckMap[ puckName];
doThis( puck);
}
}
Puck.deleteAll = function() {
Client.applyToAll( client => client.puck = null);
Puck.applyToAll( puck => {
gW.tableMap.delete( puck.b2d);
if (puck.b2d) gW.world.DestroyBody( puck.b2d);
// Tell clients that their puck is gone.
if ((puck.clientName) && (puck.clientName != 'local')) {
var control_message = {'from':'host', 'to':puck.clientName, 'data':{'puckPopped':{'value':true}} };
hC.sendSocketControlMessage( control_message);
}
});
jM.initializeModule();
gW.aT.puckMap = {};
Puck.nameIndex = 0;
pP.setPlayerCount( 0);
pP.setNpcCount( 0);
}
Puck.findCenterOfMass = function() {
// mass-weighted center of all pucks (COM)
let com_2d_m = new Vec2D(0,0);
let mass_total_kg = 0;
Puck.applyToAll( puck => {
com_2d_m = com_2d_m.add( puck.position_2d_m.scaleBy( puck.mass_kg));
mass_total_kg += puck.mass_kg;
});
com_2d_m = com_2d_m.scaleBy( 1.0 / mass_total_kg);
return com_2d_m;
}
Puck.drawSystemCenterOfMass = function( drawingContext) {
// draw circle and cross hairs at SCM if more than one puck
if (Object.keys( gW.aT.puckMap).length > 1) {
let center_2d_px = screenFromWorld( Puck.findCenterOfMass());
dF.drawCircle( drawingContext, center_2d_px, {'borderColor':'white', 'borderWidth_px':1, 'fillColor':'black', 'radius_px':3});
let dx_2d_px = new Vec2D( 8, 0);
let dy_2d_px = new Vec2D( 0, 8);
// horizontal line
dF.drawLine( drawingContext, center_2d_px.add( dx_2d_px) , center_2d_px.subtract( dx_2d_px), {'width_px':1, 'color':'white'});
// vertical line
dF.drawLine( drawingContext, center_2d_px.add( dy_2d_px) , center_2d_px.subtract( dy_2d_px), {'width_px':1, 'color':'white'});
}
}
Puck.prototype.deleteThisOne = function( pars) {
var deleteMode = setDefault( pars.deleteMode, 'fromBullet');
// Add this player's score to the summary.
if (this.clientName) gW.clients[this.clientName].addScoreToSummary('', gW.getDemoIndex(), pP.getNpcSleepUsage());
// But first, give credit to the owner of the bullet that last hit you.
// Ignore old bullets that are being removed. Don't give any credit for
// deletion by the editor. Make sure the client is still there before
// changing its score.
if ((! this.bullet) && (deleteMode != 'fromEditor')) {
if ((!Client.winnerBonusGiven) && (gW.clients[this.whoShotBullet])) {
// Give 100 for client and drone pucks, 50 for regular pucks.
if (this.clientName) {
gW.clients[this.whoShotBullet].score += 100;
} else {
gW.clients[this.whoShotBullet].score += 50;
}
}
}
// JavaScript uses garbage collection. Deleting a puck involves
// mainly nullifying all references to the puck. (Also removing references
// from the puck.)
// Note that springs are checked, in the updateAirTable function, to
// see if either of the two objects it is attached to has been deleted.
// If so, the spring is deleted. So that's not needed here.
this.deleted = true;
this.jet = null;
this.gun = null;
this.shield = null;
// Sound effect
if (! this.bullet) gW.sounds['highPop'].play();
// For pucks that are driven by clients (users or NPC)
if (this.clientName) {
if (this.clientName == 'local') {
// Must keep the local client. Just null out the puck reference in the local client.
gW.clients[this.clientName].puck = null;
} else {
// Recently decided to turn off (for now) the client disconnect when the client puck gets
// destroyed in a game of Puck Popper. So the following line is commented and then added
// the next line where the puck on the client is nulled.
//deleteRTC_onClientAndHost( this.clientName);
gW.clients[this.clientName].puck = null;
// Tell the client that his puck has been popped.
var control_message = {'from':'host', 'to':this.clientName, 'data':{'puckPopped':{'value':true}} };
hC.sendSocketControlMessage( control_message);
// Remove the client if it's an NPC.
if (this.clientName.slice(0,3) == 'NPC') {
delete gW.clients[ this.clientName];
pP.addToNpcCount( -1);
}
}
pP.addToPlayerCount( -1);
}
// Filter out any reference to this jello puck in the jelloPuck array.
if (this.jello) {
jM.removeDeletedPucks();
}
// Delete the corresponding Box2d object.
gW.tableMap.delete( this.b2d);
gW.world.DestroyBody( this.b2d);
// Remove this puck from our puck map.
delete gW.aT.puckMap[ this.name];
// ...and from the multi-select map.
gW.hostMSelect.removeOne( this);
}
Puck.prototype.copyThisOne = function( pars) {
// If the position is not specified in pars, put the copy at the same position as the original.
var position_2d_m = setDefault( pars.position_2d_m, this.position_2d_m);
// Make a copy of the mutable objects that are passed into the Puck constructor.
var p_2d_m = Object.assign({}, position_2d_m);
var v_2d_mps = Object.assign({}, this.velocity_2d_mps);
var parsForNewBirth = Object.assign({}, this.parsAtBirth);
// Make sure the name is nulled so the auto-naming feature is used in the constructor.
parsForNewBirth.name = null;
// Don't allow any network client or NPC features.
parsForNewBirth.clientName = null;
parsForNewBirth.pinName = null;
/*
Update pars to reflect any edits or state changes. For example,
this loop, for the first element in the array, does the following:
parsForNewBirth.angle_r = this.angle_r;
*/
var parsToCopy = ['angle_r','angularSpeed_rps','friction','restitution','linDamp','angDamp','bullet_restitution','jello'];
for (var i = 0, len = parsToCopy.length; i < len; i++) {
parsForNewBirth[ parsToCopy[i]] = this[ parsToCopy[i]];
}
if (this.shape == 'circle') {
parsForNewBirth.radius_m = this.radius_m;
} else {
parsForNewBirth.half_height_m = this.half_height_m;
parsForNewBirth.half_width_m = this.half_width_m;
}
// If this is a drone puck, make a new NPC client for the copy.
if (this.clientName && (this.clientName.slice(0,3) == 'NPC')) {
// Sync the navigation timer of the copy to that of the original.
// Note: instantiating with the current NPC name will increment the NPC counter (and the name).
var theClientForTheCopy = new Client({'name':this.clientName, 'color':'purple',
'NPC_pin_timer_s':gW.clients[this.clientName].NPC_pin_timer_s,
'NPC_pin_timer_limit_s':gW.clients[this.clientName].NPC_pin_timer_limit_s});
// Add the client name to the birth parameters for the puck.
parsForNewBirth.clientName = theClientForTheCopy.name;
}
// Reset the group index so the copy of a client puck will collide with the original.
// Added this to deal with copies of client pucks that have a negative group index (that prevents collisions with their bullets, self shooting).
// Want to be able to copy a client's puck but have it collide normally. Without this statement,
// 7b, or any PP capture, would produce a copy that would not collide with the source puck.
if (this.clientName) parsForNewBirth.groupIndex = 0;
// A copy of an NPC client puck will be fully outfitted (a client name is provided).
// A copy of regular client puck will be only a puck shell (no client name).
var newPuck = new Puck( p_2d_m, v_2d_mps, parsForNewBirth);
if (newPuck.jello) jM.addPuck( newPuck);
return newPuck;
}
Puck.prototype.updateState = function() {
this.getPosition();
this.getVelocity();
this.getAngle();
this.getAngularSpeed();
}
Puck.prototype.setPosition = function( pos_2d_m, angle_deg) {
this.position_2d_m = pos_2d_m;
this.position_2d_px = screenFromWorld( this.position_2d_m);
this.b2d.SetPosition( pos_2d_m);
this.velocity_2d_mps = new Vec2D(0.0,0.0);
this.b2d.SetLinearVelocity( new Vec2D(0.0,0.0));
this.angularSpeed_rps = 0.0;
this.b2d.SetAngularVelocity( this.angularSpeed_rps);
this.b2d.SetAngle( angle_deg * (Math.PI/180.0));
}
Puck.prototype.create_Box2d_Puck = function() {
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_dynamicBody;
// Create the body and a fixture.
this.b2d = gW.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();
this.b2d.SetLinearDamping( this.linDamp);
this.b2d.SetAngularDamping( this.angDamp);
this.b2d.SetBullet( this.bullet);
//this.b2d.SetSleepingAllowed( false);
this.b2d.m_fixtureList.SetFriction( this.friction);
this.b2d.m_fixtureList.SetRestitution( this.restitution);
}
Puck.prototype.modify_Box2d_BodyAndFixture = function( fixture, pars) {
// "fixture" can be either a created fixture object or a fixture definition (for use in creation).
// Note that the default behavior is to have all scaling factors at 1.0 which only updates the box2d attributes
// to correspond to those of the Puck object.
this.restitution_scaling = setDefault( pars.restitution_scaling, 1.0);
this.friction_scaling = setDefault( pars.friction_scaling, 1.0);
this.linDamp_scaling = setDefault( pars.linDamp_scaling, 1.0);
this.angDamp_scaling = setDefault( pars.angDamp_scaling, 1.0);
// Adjust elasticity (bounciness).
if (this.restitution_scaling != 1.0) {
// If restitution is zero, bump it up a little so the scaling factor has something to work with.
if (this.restitution == 0.0) this.restitution = 0.01;
// Apply the scaling factor.
this.restitution *= this.restitution_scaling;
// Keep it between 0.0 and 1.0.
if (this.restitution > 1.00) this.restitution = 1.0;
if (this.restitution < 0.01) this.restitution = 0.0;
// Keep this new restitution value independent of the gravity toggle.
this.restitution_fixed = true;
gW.messages['help'].newMessage("[base,yellow]" + this.name + "[base] restitution = " + this.restitution.toFixed(4), 0.5);
}
// If this fixture has been created, then use Set function.
if (fixture.SetRestitution) {
fixture.SetRestitution( this.restitution);
} else {
fixture.restitution = this.restitution;
}
// Adjust friction (surface tackiness).
if (this.friction_scaling != 1.0) {
// If friction is zero, bump it up a little so the scaling factor has something to work with.
if (this.friction == 0.0) this.friction = 0.01;
// Apply the scaling factor.
this.friction *= this.friction_scaling;
// Stop at zero.
if (this.friction < 0.01) this.friction = 0.0;
// Keep this new friction value independent of the gravity toggle.
this.friction_fixed = true;
gW.messages['help'].newMessage("[base,yellow]" + this.name + "[base] friction = " + this.friction.toFixed(4), 0.5);
}
// If this fixture has been created, then use Set function.
if (fixture.SetFriction) {
fixture.SetFriction( this.friction);
} else {
fixture.friction = this.friction;
}
// Adjust linear damping (damping from fluid drag).
if (this.linDamp_scaling != 1.0) {
// If linear damping is zero, bump it up a little so the scaling factor has something to work with.
if (this.linDamp == 0.0) this.linDamp = 0.01;
// Apply the scaling factor.
this.linDamp *= this.linDamp_scaling;
// Stop at zero.
if (this.linDamp < 0.01) this.linDamp = 0.0;
gW.messages['help'].newMessage("[base,yellow]" + this.name + "[base] drag coefficient = " + this.linDamp.toFixed(4), 0.5);
}
// Note: linearDamping is a body property (not fixture property)
this.b2d.SetLinearDamping( this.linDamp);
// Adjust angular damping (damping from fluid drag).
if (this.angDamp_scaling != 1.0) {
// If angular damping is zero, bump it up a little so the scaling factor has something to work with.
if (this.angDamp == 0.0) this.angDamp = 0.01;
// Apply the scaling factor.
this.angDamp *= this.angDamp_scaling;
// Stop at zero.
if (this.angDamp < 0.01) this.angDamp = 0.0;
gW.messages['help'].newMessage("[base,yellow]" + this.name + "[base] rotational drag coefficient = " + this.angDamp.toFixed(4), 0.5);
}
// Note: AngularDamping is a body property (not fixture property)
this.b2d.SetAngularDamping( this.angDamp);
}
Puck.prototype.define_fixture = function( pars) {
// Note that the default behavior is to have all scaling factors at 1.0 which only updates the box2d attributes
// to correspond to those of the Puck object.
this.width_scaling = setDefault( pars.width_scaling, 1.0);
this.height_scaling = setDefault( pars.height_scaling, 1.0);
this.radius_scaling = setDefault( pars.radius_scaling, 1.0);
// Create a circular or polygon dynamic box2d fixture.
var fixDef = new b2FixtureDef;
fixDef.density = this.density;
// Adjust some of the attributes of the fixture definition.
this.modify_Box2d_BodyAndFixture( fixDef, pars);
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 (except for gun bullets).
Note this chokes off an editing series in a discontinuous way. So this choke is not allowed
for a shooting puck in cannibalize mode. In that case, size restrictions must be done at time
of shooting (see the restrictions in fireBullet in the puckPopper module). Otherwise the size
reduction won't agree with the size of the bullet.
*/
if ( (this.radius_px < Puck.minRadius_px) && ( ! this.gunBullet()) && ( ! this.cannibalize) ) {
this.radius_px = Puck.minRadius_px;
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 (polygon) 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;
// Make it a rectangle.
fixDef.shape.SetAsBox(this.half_width_m, this.half_height_m);
}
return fixDef;
}
Puck.prototype.interpret_editCommand = function( command, sf = null) {
// For editing puck characteristics:
// sf (specific factor) is used to override the default scaling factors.
// Note: to modify the fixture dimensions you must 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;
var restitution_factor = 1.0;
var friction_factor = 1.0;
var drag_factor = 1.0;
var angDrag_factor = 1.0;
if (command == 'wider') {
width_factor = (sf) ? sf : 1.1;
} else if (command == 'thinner') {
width_factor = (sf) ? sf : 1.0/1.1;
} else if (command == 'taller') {
height_factor = (sf) ? sf : 1.1;
} else if (command == 'shorter') {
height_factor = (sf) ? sf : 1.0/1.1;
} else if (command == 'moreDamping') {
restitution_factor = (sf) ? sf : 1.0/1.05;
} else if (command == 'lessDamping') {
restitution_factor = (sf) ? sf : 1.05;
} else if (command == 'moreFriction') {
friction_factor = (sf) ? sf : 1.05;
} else if (command == 'lessFriction') {
friction_factor = (sf) ? sf : 1.0/1.05;
} else if (command == 'moreDrag') {
drag_factor = (sf) ? sf : 1.05;
} else if (command == 'lessDrag') {
drag_factor = (sf) ? sf : 1.0/1.05;
} else if (command == 'moreAngDrag') {
angDrag_factor = (sf) ? sf : 1.05;
} else if (command == 'lessAngDrag') {
angDrag_factor = (sf) ? sf : 1.0/1.05;
} else if (command == 'noChange') {
// don't change anything.
}
if (['wider','thinner','taller','shorter'].includes( command)) {
// Changes to the dimensions of the fixture require the fixture to be deleted and re-created.
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();
if ((height_factor != 1.0) || (width_factor != 1.0)) {
if (this.shape == "circle") {
var dimensionsReport = "\\ radius = " + this.radius_m.toFixed(3) + " m";
} else {
var dimensionsReport = "\\ half width, half height = " + this.half_width_m.toFixed(3) + ", " + this.half_height_m.toFixed(3) + " m";
}
// inhibit this message when a sf factor is provided (like when a shooting puck is in cannibalize mode)
if ( ! sf) gW.messages['help'].newMessage("[base,yellow]" + this.name + "[base] mass = " + this.mass_kg.toFixed(3) + " kg" + dimensionsReport, 1.0);
}
// This step (after deleting and recreating the fixture) keeps the pucks from falling (penetrating) into the floor when gravity is on.
// It's an odd buggy behavior that only lasts a few frames and then the engine seems to realize the object should be colliding with the floor.
// This call to SetPosition somehow gets the collisions working immediately after the fixture violence. Surprising that this body procedure is necessary
// since it was the fixture not the body, that was deleted.
this.b2d.SetPosition( this.position_2d_m);
} else {
// Non-dimensional changes can be made directly to the box2d body and fixture attributes.
let scalingFactors = {'restitution_scaling':restitution_factor, 'friction_scaling':friction_factor, 'linDamp_scaling':drag_factor, 'angDamp_scaling':angDrag_factor};
this.modify_Box2d_BodyAndFixture( this.b2d.m_fixtureList, scalingFactors);
}
// If there's a spring that has one (or both) of its ends attached to THIS puck,
// and it's a b2d spring, update that spring.
Spring.applyToAll( spring => {
if (((this == spring.spo1) || (this == spring.spo2)) && (spring.softConstraints)) {
spring.updateB2D_spring();
}
});
// Update the puck tail radius.
if (this.tail) {
this.tail.initial_radius_m = this.radius_m;
}
}
Puck.prototype.gunBullet = function() {
return (this.bullet && this.ageLimit_ms);
}
Puck.prototype.regularBullet = function() {
return (this.bullet && ( ! this.ageLimit_ms));
}
Puck.prototype.getPosition = function() {
this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition());
}
Puck.prototype.worldPoint = function( localPoint_2d) {
return Vec2D_from_b2Vec2( this.b2d.GetWorldPoint( localPoint_2d));
}
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.drawClientName = function( drawingContext, deltaT_s, pars = {}) {
this.stayOn = setDefault( pars.stayOn, false);
if ((this.nameTip_timer_s < this.nameTip_timerLimit_s) || this.stayOn) {
this.nameTip_timer_s += deltaT_s;
var font = setDefault( pars.font, "20px Arial");
var color = setDefault( pars.color, 'lightgray');
if (gW.clients[this.clientName].nickName) {
var name = gW.clients[this.clientName].nickName;
} else {
var name = Client.translateIfLocal(this.clientName);
}
drawingContext.font = font;
drawingContext.fillStyle = color;
drawingContext.textAlign = "center";
var x_px = this.position_2d_px.x;
var y_px = this.position_2d_px.y - (this.radius_px * 1.20);
drawingContext.fillText( name, x_px, y_px);
}
}
Puck.prototype.drawImage = function( drawingContext, imageID, imageScale, alpha, rotateWithPuck=false) {
var img = document.getElementById( imageID);
let imageWidth_px, imageHeight_px, offset_x_px, offset_y_px;
if (this.shape == 'circle') {
imageWidth_px = (this.radius_px * 2) * imageScale;
imageHeight_px = imageWidth_px;
// (my) offset is the distance from center to top left edge of image: half width and half height.
offset_x_px = imageWidth_px/2.0;
offset_y_px = offset_x_px;
} else {
imageWidth_px = (this.half_width_px * 2) * imageScale;
imageHeight_px = (this.half_height_px * 2) * imageScale;
// (my) offset is the distance from center to top left edge of image: half width and half height.
offset_x_px = imageWidth_px/2.0;
offset_y_px = imageHeight_px/2.0;
}
drawingContext.globalAlpha = alpha;
if (rotateWithPuck) {
// Must translate before doing the rotation
drawingContext.translate( this.position_2d_px.x, this.position_2d_px.y);
drawingContext.rotate( -this.angle_r);
drawingContext.drawImage( img, -offset_x_px, -offset_y_px, imageWidth_px, imageHeight_px);
drawingContext.rotate( this.angle_r);
drawingContext.translate( -this.position_2d_px.x, -this.position_2d_px.y);
} else {
// left edge--------------------------, top edge---------------------------,
drawingContext.drawImage( img, this.position_2d_px.x - offset_x_px, this.position_2d_px.y - offset_y_px, imageWidth_px, imageHeight_px);
}
drawingContext.globalAlpha = 1.00;
}
Puck.prototype.draw = function( drawingContext, deltaT_s) {
this.position_2d_px = screenFromWorld( this.position_2d_m);
var borderColor, fillColor, fillAlpha;
// Exit if puck is to be invisible during PauseErase.
if ( gW.getPauseErase() && ( ! this.drawDuringPE) ) return;
// Refer to index.html for the hidden image elements that are needed for each imageID (search on "puck costumes").
if (this.shape == 'circle') {
if ($('#chkC19').prop('checked') && (this.clientName) && (this.clientName.slice(0,3) == 'NPC')) {
// Virus scale, alpha, rotate
this.drawImage( drawingContext, 'covid', 1.65, 1.00, false);
fillColor = this.color;
fillAlpha = 1.00;
} else if (this.imageID) {
this.drawImage( drawingContext, this.imageID, this.imageScale, 1.00, true);
fillColor = this.color;
fillAlpha = 0.00;
} else if (this.hsl) {
fillColor = this.hsl.colorString();
this.hsl.step();
fillAlpha = 1.00;
} else {
fillColor = this.color;
fillAlpha = 1.00;
}
// 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 = this.borderColor;
}
if ((this.bullet) && (this.bulletIndication)) fillColor = 'black';
this.drawCircle( drawingContext, this.position_2d_px,
{'borderColor':borderColor, 'borderWidth_px':this.borderWidth_px, 'fillColor':fillColor, 'radius_px':this.radius_px, 'fillAlpha':fillAlpha} );
// Draw the health circle.
this.poorHealthFraction = this.hitCount / this.hitLimit;
var poorHealthRadius = this.radius_px * this.poorHealthFraction;
if (poorHealthRadius > 0) {
this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'chocolate', 'radius_px':poorHealthRadius} );
}
if (gW.clients[this.clientName]) {
// Update and draw the shield.
this.shield.updateState( drawingContext, deltaT_s);
// Draw the client finder circle. Big fat one. Easy to see. So you can find your puck.
if (gW.clients[this.clientName].key_questionMark == "D") {
gW.clients[this.clientName].puck.nameTip_timer_s = 0.0;
this.drawCircle( drawingContext, this.position_2d_px,
{'borderColor':gW.clients[this.clientName].color,
'fillColor':'noFill',
'borderWidth_px':this.radius_px * 0.3,
'radius_px' :this.radius_px * 1.5 } );
}
}
// Show rotational orientation: draw a line segment along the line from the center out to a local point on the radius.
// Don't show this line for client pucks (gun) in Puck Popper.
if (!this.gun && this.angleLine) {
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)) ) );
this.drawLine( drawingContext, pointAtHalfRadius_2d_px, pointOnEdge_2d_px, {'width_px':2, 'color':'white'});
//this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':'white', 'borderWidth_px':0, 'fillColor':'white', 'radius_px':1} );
}
// Draw the tail if we have one.
if (this.tail) this.tail.update( drawingContext, this.position_2d_m, deltaT_s);
// If pool game:
if (gW.getDemoVersion().slice(0,3) == "3.d") {
// label pucks, draw stripe, draw finder circle
gB.drawPoolBallFeatures( drawingContext, this);
}
} else {
// Draw the rectangle.
// This border width adjustment gives a similar result for rectangles when compared with the circle case.
if (this.borderWidth_px >= 2) {
var rect_borderWidth_px = this.borderWidth_px - 1;
} else if (this.borderWidth_px < 2) {
var rect_borderWidth_px = this.borderWidth_px;
}
var fillColor = ((this.bullet) && (this.bulletIndication)) ? 'black' : this.color;
if (this.imageID) {
this.drawImage( drawingContext, this.imageID, this.imageScale, 1.00, true);
var rect_borderWidth_px = 0;
var lineAlpha = 0.0;
var fillIt = false;
} else {
var lineAlpha = 1.0;
var fillIt = true;
}
this.drawPolygon( drawingContext, gW.b2d_getPolygonVertices_2d_px( this.b2d),
{'borderColor':this.borderColor,'borderWidth_px':rect_borderWidth_px,'fillColor':fillColor,'fillIt':fillIt,'lineAlpha':lineAlpha});
}
if ((this.clientName) && !(this.clientName.slice(0,3) == 'NPC')) {
this.drawClientName( drawingContext, deltaT_s);
}
}
Puck.prototype.draw_MultiSelectPoint = function( drawingContext) {
var selectionPoint_2d_px;
if ( ! gW.dC.comSelection.checked) {
selectionPoint_2d_px = screenFromWorld( this.b2d.GetWorldPoint( this.selectionPoint_l_2d_m));
} else {
selectionPoint_2d_px = this.position_2d_px;
}
this.drawCircle( drawingContext, selectionPoint_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5});
}
Puck.prototype.applyForces = function( deltaT_s) {
// Net resulting force on the puck.
// First consider forces acting on the COM.
// F = acceleration * mass
var g_force_2d_N = Puck.g_2d_mps2.scaleBy( this.mass_kg);
var puck_forces_2d_N = g_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/deltaT_s));
if ( ! this.tempInhibitExtForce) {
// 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. The spring forces are this array.
for (var j = 0, len = this.nonCOM_2d_N.length; j < len; j++) {
this.b2d.ApplyForce( this.nonCOM_2d_N[j].force_2d_N, this.nonCOM_2d_N[j].point_w_2d_m);
}
/*
// Apply torques. #b2d
*/
} else {
this.tempInhibitExtForce = false;
}
// Now reset the aggregate forces.
this.sprDamp_force_2d_N = new Vec2D(0.0,0.0);
this.nonCOM_2d_N = [];
this.impulse_2d_Ns = new Vec2D(0.0,0.0);
}
// Static spring anchors (no collisions)
function Pin( position_2d_m, pars) {
DrawingFunctions.call(this) // inherit
this.parsAtBirth = pars;
//this.alsoThese = [];
this.cursorPin = setDefault( pars.cursorPin, false);
if (pars.name) {
this.name = pars.name;
// Get the number part of the name
var numberInName = this.name.slice(3);
// Don't change the index if no number in name.
if (isNaN( numberInName)) {
numberInName = 0;
} else {
numberInName = Number( numberInName);
}
Pin.nameIndex = Math.max( Pin.nameIndex, numberInName);
} else {
Pin.nameIndex += 1;
this.name = 'pin' + Pin.nameIndex;
}
// Don't put cursor pins in the map.
if (!this.cursorPin) gW.aT.pinMap[this.name] = this;
this.position_2d_m = Vec2D_check( position_2d_m);
this.position_2d_px = screenFromWorld( this.position_2d_m);
// Local selection point for a pin is always at its center.
this.selectionPoint_l_2d_m = new Vec2D(0.0, 0.0);
this.velocity_2d_mps = setDefault( pars.velocity_2d_mps, new Vec2D(0.0, 0.0));
this.radius_px = setDefault( 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);
// www.iforce2d.net/b2dtut/collision-filtering
this.groupIndex = setDefault( pars.groupIndex, 0);
this.categoryBits = setDefault( pars.categoryBits, 0x0001);
// Masking parameters for b2d object for the pin:
// The default Box2D values are 0xFFFF for maskBits (collide with everything).
// Default here, 0x0000, will prevent collisions with the pin (collide with nothing).
this.maskBits = setDefault( pars.maskBits, 0x0000);
this.deleted = false;
// For creating a circular linked-list of pins to guide the NPC movement.
this.NPC = setDefault( pars.NPC, false);
this.nextPinName = setDefault( pars.nextPinName, null);
this.previousPinName = setDefault( pars.previousPinName, null);
this.visible = setDefault( pars.visible, true);
this.color = setDefault( pars.color, 'blue');
this.borderColor = setDefault( pars.borderColor, 'gray');
this.navLineColor = setDefault( pars.navLineColor, 'white');
// All parameters should be set above this point.
this.b2d = null;
this.create_b2d_pin();
// Create a reference back to this pin from the b2d pin.
gW.tableMap.set(this.b2d, this);
}
Pin.nameIndex = 0;
Pin.applyToAll = function ( doThis) {
for (var pinName in gW.aT.pinMap) {
var pin = gW.aT.pinMap[ pinName];
doThis( pin);
}
}
Pin.deleteAll = function () {
Pin.applyToAll( pin => {
gW.tableMap.delete( pin.b2d);
if (pin.b2d) gW.world.DestroyBody( pin.b2d);
});
gW.aT.pinMap = {};
Pin.nameIndex = 0;
}
Pin.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
Pin.prototype.constructor = Pin; // Rename the constructor (after inheriting)
Pin.prototype.deleteThisOne = function( pars) {
var deleteMode = setDefault( pars.deleteMode, null);
// Note that springs are checked, in the updateAirTable function, to
// see if either of the two objects it is attached to has been deleted.
// If so, the spring is deleted. So that's not needed here.
// Reassign the surrounding pins (if they are part of an NPC path)
if (this.NPC) {
// Point the next pin back at the previous pin.
gW.aT.pinMap[this.nextPinName].previousPinName = gW.aT.pinMap[this.previousPinName].name;
// Point the previous pin forward to the next pin.
gW.aT.pinMap[this.previousPinName].nextPinName = gW.aT.pinMap[this.nextPinName].name;
}
// Delete reference in the tableMap.
gW.tableMap.delete( this.b2d);
// Delete the corresponding Box2d object.
gW.world.DestroyBody( this.b2d);
// Mark this pin as deleted.
this.deleted = true;
// Remove this pin from the pin map.
delete gW.aT.pinMap[ this.name];
// ...and from the multi-select map.
gW.hostMSelect.removeOne( this);
}
Pin.prototype.copyThisOne = function( pars) {
var position_2d_m = setDefault( pars.position_2d_m, this.position_2d_m);
var p_2d_m = Object.assign({}, position_2d_m);
var parsForNewBirth = Object.assign({}, this.parsAtBirth);
// Make sure the name is nulled so the auto-naming feature is used in the constructor.
parsForNewBirth.name = null;
var newPin = new Pin( p_2d_m, parsForNewBirth);
// Slide the new pin in front of the old one if it's in a NPC.
if (this.NPC) {
// Set the two links for the new pin.
newPin.nextPinName = this.nextPinName;
newPin.previousPinName = this.name;
// Update the backward link of the original next pin.
gW.aT.pinMap[this.nextPinName].previousPinName = newPin.name;
// Update the forward link of the original pin.
this.nextPinName = newPin.name;
}
return newPin;
}
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 = gW.world.CreateBody( bodyDef);
this.b2d.CreateFixture( this.define_fixture());
// Set the state: position.
this.b2d.SetPosition( this.position_2d_m);
this.b2d.SetLinearVelocity( this.velocity_2d_mps);
}
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.setPosition = function( newPosition_2d_m) {
this.b2d.SetPosition( newPosition_2d_m);
this.position_2d_m = newPosition_2d_m;
this.position_2d_px = screenFromWorld( this.position_2d_m);
}
Pin.prototype.draw_MultiSelectPoint = function( drawingContext) {
this.getPosition();
this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5});
}
Pin.prototype.draw = function( drawingContext, radius_px) {
radius_px = setDefault( radius_px, this.radius_px);
if (gW.dC.editor.checked || this.visible) {
this.getPosition();
// use white for the color-mixing demo (#9).
var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? 'white' : this.color;
if ( ! gW.getPauseErase()) {
this.drawCircle( drawingContext, this.position_2d_px, {'borderColor':this.borderColor, 'borderWidth_px':2, 'fillColor':fillColor, 'radius_px':radius_px});
}
}
// Draw lines to indicate the relationships in the NPC navigation map.
if (this.NPC && gW.dC.editor.checked) {
if (gW.aT.pinMap[this.nextPinName]) {
this.drawLine( drawingContext, this.position_2d_px, gW.aT.pinMap[this.nextPinName].position_2d_px, {'width_px':1, 'color':this.navLineColor, 'dashArray':[3]});
}
}
}
function Joint( tableObject1, tableObject2, pars) {
DrawingFunctions.call(this); // inherit
// Must have both objects to attach the Joint.
// Throwing an error forces an exit from this constructor.
if ( !((tableObject1) && (tableObject2)) ) {
var errorObj = new Error('Attempting to construct a joint with one or both connected objects missing.');
errorObj.name = 'from Joint constructor';
throw errorObj;
}
this.parsAtBirth = pars;
if (pars.name) {
this.name = pars.name;
// Get the number part of the name
var numberInName = this.name.slice(1);
// Don't change the index if no number in name.
if (isNaN( numberInName)) {
numberInName = 0;
} else {
numberInName = Number( numberInName);
}
Joint.nameIndex = Math.max( Joint.nameIndex, numberInName);
} else {
Joint.nameIndex += 1;
this.name = 'j' + Joint.nameIndex;
}
// Add this joint to the joint map.
gW.aT.jointMap[this.name] = this;
this.color = setDefault( pars.color, "black");
this.visible = setDefault( pars.visible, true);
this.enableLimit = setDefault( pars.enableLimit, false);
this.lowerLimit_deg = setDefault( pars.lowerLimit_deg, null);
this.upperLimit_deg = setDefault( pars.upperLimit_deg, null);
this.colorInTransition = true;
this.selected = setDefault( pars.selected, false);
// Joint Table Object (jto1). Giving this a distinctive name so that it can be (if needed) filtered
// out in the JSON capture. This filtering avoids some wordiness in the capture.
this.jto1 = tableObject1;
this.jto1_name = tableObject1.name;
// local point where spring is attached on jto1
this.jto1_ap_l_2d_m = setDefault( pars.jto1_ap_l_2d_m, new Vec2D(0,0));
// Same reasoning here for the distinctive name (jto2, not p2).
this.jto2 = tableObject2;
this.jto2_name = tableObject2.name;
// local point where spring is attached on jto2
this.jto2_ap_l_2d_m = setDefault( pars.jto2_ap_l_2d_m, new Vec2D(0,0));
// All parameters should be set above this point.
this.b2d = null;
this.createJoint();
}
Joint.nameIndex = 0;
Joint.countInMultiSelect = 0;
Joint.applyToAll = function ( doThis) {
// Run the doThis code on each joint.
for (var jointName in gW.aT.jointMap) {
var joint = gW.aT.jointMap[ jointName];
var result = doThis( joint);
if (result && result['breakRequest']) break;
}
}
Joint.checkIfAttached = function ( objName) {
let yesFoundIt = false;
Joint.applyToAll( joint => {
if ( (joint.jto1.name == objName) || (joint.jto2.name == objName) ) {
yesFoundIt = true;
return {'breakRequest':true};
}
});
return yesFoundIt;
}
Joint.deleteAll = function () {
// Remove these joints from the b2d world.
Joint.applyToAll( joint => {
if (joint.softConstraints) {
gW.world.DestroyJoint( joint.b2d);
joint.b2d = null;
}
});
gW.aT.jointMap = {};
Joint.nameIndex = 0;
}
Joint.findAll_InMultiSelect = function ( doThis) {
// Find all the joints that have both ends in the multi-select map.
Joint.countInMultiSelect = 0;
Joint.applyToAll( joint => {
if (joint.inMultiSelect()) {
Joint.countInMultiSelect++;
// For each joint you find.
doThis( joint);
}
});
}
Joint.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
Joint.prototype.constructor = Spring; // Rename the constructor (after inheriting)
Joint.prototype.setEnableLimit = function( trueFalse) {
this.enableLimit = trueFalse;
this.b2d.EnableLimit( trueFalse);
}
Joint.prototype.setLimits = function(lowerLimit_deg, upperLimit_deg) {
this.lowerLimit_deg = lowerLimit_deg;
this.upperLimit_deg = upperLimit_deg;
this.b2d.SetLimits( lowerLimit_deg * Math.PI/180, upperLimit_deg * Math.PI/180);
}
Joint.prototype.createJoint = function() {
var joint = new b2RevoluteJointDef;
// Identify the connected bodies.
joint.bodyA = this.jto1.b2d;
joint.bodyB = this.jto2.b2d;
// Connect to the attachment point on each body.
joint.localAnchorA = b2Vec2_from_Vec2D( this.jto1_ap_l_2d_m);
joint.localAnchorB = b2Vec2_from_Vec2D( this.jto2_ap_l_2d_m);
// Will the connected bodies collide?
joint.collideConnected = false;
// Add the joint to the world. And keep a reference to it here (this joint) as b2d.
this.b2d = gW.world.CreateJoint( joint);
this.b2d.EnableLimit( this.enableLimit);
this.b2d.SetLimits( this.lowerLimit_deg * Math.PI/180, this.upperLimit_deg * Math.PI/180);
}
Joint.prototype.deleteThisOne = function( pars) {
var deleteMode = setDefault( pars.deleteMode, null);
gW.world.DestroyJoint( this.b2d);
this.b2d = null;
delete gW.aT.jointMap[ this.name];
}
Joint.prototype.copyThisOne = function(to1, to2) {
// Make a copy of the mutable objects that are passed into the Joint constructor.
var pars = Object.assign({}, this.parsAtBirth);
// Null the name so the auto-naming feature is used in the constructor.
pars.name = null;
// Note that this instantiation adds this new joint to the joint map.
var tempJoint = new Joint( to1, to2, pars);
return tempJoint.name;
}
Joint.prototype.updateWorldPositions = function() {
// Find the world position of the attachment points.
// if not attached to the center
if ( ! this.jto1_ap_l_2d_m.zeroLength() ) {
this.jto1_ap_w_2d_m = Vec2D_from_b2Vec2( this.jto1.b2d.GetWorldPoint( this.jto1_ap_l_2d_m));
// if attached to the center
} else {
this.jto1_ap_w_2d_m = this.jto1.position_2d_m;
}
this.jto1_ap_w_2d_px = screenFromWorld( this.jto1_ap_w_2d_m);
}
Joint.prototype.inMultiSelect = function() {
if ((this.jto1.name in gW.hostMSelect.map) && (this.jto2.name in gW.hostMSelect.map)) {
return true;
} else {
return false;
}
}
Joint.prototype.report = function( ) {
let enableLimitString = (this.enableLimit) ? "on":"off";
let lowerLimitString = (this.lowerLimit_deg) ? this.lowerLimit_deg.toFixed(0) : "n/a";
let upperLimitString = (this.upperLimit_deg) ? this.upperLimit_deg.toFixed(0) : "n/a";
let messageString = "revolute joint: [base,yellow]" + this.name + "[base]" +
"\\ angle limits (" + enableLimitString + "): " + lowerLimitString + " to " + upperLimitString +
"\\ angle = " + (this.b2d.GetJointAngle() * 180.0/Math.PI).toFixed(0);
// For the case where there is a single spring and a single revolute joint, add the revolute report to the end of the spring report.
if (gW.messages['help2'].message.includes('spring')) {
gW.messages['help2'].addToIt( "\\ \\" + messageString, {'additionalTime_s':0.0});
} else {
gW.messages['help2'].newMessage( messageString, 0.05);
}
}
Joint.prototype.draw = function(drawingContext) {
this.updateWorldPositions();
// If this.color is not black, it will be used as the fill color (to get your attention) until the 3 second timer ends.
var transitionColor;
if (this.colorInTransition) {
transitionColor = this.color;
} else {
transitionColor = 'black';
}
window.setTimeout( function() {
this.colorInTransition = false;
}.bind(this), 3000);
if ( (this.visible) && ( ! gW.getPauseErase()) ) {
let fillColor, borderColor;
let outerRadius_px = 5;
if (this.selected) {
fillColor = 'yellow';
borderColor = 'black';
} else {
if (this.name == gW.hostMSelect.candidateReportPasteDelete) {
fillColor = 'white';
borderColor = 'black'
outerRadius_px = 8;
} else {
fillColor = transitionColor;
borderColor = 'white'
}
}
// larger circle on the bottom
this.drawCircle( drawingContext, this.jto1_ap_w_2d_px, {'borderColor':borderColor, 'borderWidth_px':1, 'fillColor':fillColor, 'radius_px':outerRadius_px});
// smaller circle on top
this.drawCircle( drawingContext, this.jto1_ap_w_2d_px, {'borderColor':borderColor, 'borderWidth_px':1, 'fillColor':fillColor, 'radius_px':3});
}
}
function Spring( puckOrPin1, puckOrPin2, pars) {
DrawingFunctions.call(this); // inherit
// Must have both objects to attach the spring.
// Throwing an error forces an exit from this constructor.
if ( !((puckOrPin1) && (puckOrPin2)) ) {
var errorObj = new Error('Attempting to construct a spring with one or both connected objects missing.');
errorObj.name = 'from Spring constructor';
throw errorObj;
}
this.parsAtBirth = pars;
if (pars.name) {
this.name = pars.name;
// Get the number part of the name
var numberInName = this.name.slice(1);
// Don't change the index if no number in name.
if (isNaN( numberInName)) {
numberInName = 0;
} else {
numberInName = Number( numberInName);
}
Spring.nameIndex = Math.max( Spring.nameIndex, numberInName);
} else {
Spring.nameIndex += 1;
this.name = 's' + Spring.nameIndex;
}
gW.aT.springMap[this.name] = this;
this.color = setDefault( pars.color, "red");
this.visible = setDefault( pars.visible, true);
this.length_m = setDefault( pars.length_m, 0.0);
this.stretch_m = setDefault( pars.stretch_m, 0.0);
this.strength_Npm = setDefault( pars.strength_Npm, 0.5); // 60.0
this.unstretched_width_m = setDefault( pars.unstretched_width_m, 0.025);
// Note that pucks have an attribute linDamp, with an effect similar to drag_c. Both can be
// used to model a drag force on the pucks at the end of the spring.
this.drag_c = setDefault( pars.drag_c, 0.0);
this.damper_Ns2pm2 = setDefault( pars.damper_Ns2pm2, 0.5);
this.selected = setDefault( pars.selected, false);
this.navigationForNPC = setDefault( pars.navigationForNPC, false);
this.forCursor = setDefault( pars.forCursor, false);
// Spring-puck/pin 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 = puckOrPin1;
this.p1_name = puckOrPin1.name;
// Pin one end of the spring to a fixed location.
if (this.spo1.constructor.name == "Pin") {
this.pinned = true;
} else {
this.pinned = false;
}
// local point where spring is attached on spo1
this.spo1_ap_l_2d_m = setDefault( pars.spo1_ap_l_2d_m, new Vec2D(0,0));
// 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.pinned = true;
} else {
this.pinned = false;
}
// local point where spring is attached on spo2
this.spo2_ap_l_2d_m = setDefault( pars.spo2_ap_l_2d_m, new Vec2D(0,0));
this.p1p2_separation_2d_m = new Vec2D(0,0);
this.p1p2_separation_m = 0;
this.p1p2_normalized_2d = new Vec2D(0,0);
this.fixedLength = setDefault( pars.fixedLength, false);
this.collideConnected = setDefault( pars.collideConnected, true);
// All parameters should be set above this point.
// To model the spring as a distance joint in b2d.
// Don't allow this for the navigation springs.
// If fixedLength is specified, make sure
// that softConstraints (i.e. the distance joint) is also specified because it is
// needed for modeling the fixed-length join.
this.b2d = null;
if (this.fixedLength) {
this.softConstraints_setInPars = true;
this.softConstraints = true;
} else {
this.softConstraints_setInPars = (typeof pars.softConstraints !== "undefined") ? true : false;
this.softConstraints = setDefault( pars.softConstraints, gW.getSoftConstraints_default());
}
if (this.softConstraints && ( ! this.navigationForNPC)) {
this.createDistanceJoint();
} else if (this.navigationForNPC) {
this.softConstraints = false;
}
}
Spring.nameIndex = 0;
Spring.nameForPasting = null;
Spring.countInMultiSelect = 0;
Spring.applyToAll = function ( doThis) {
// Run the doThis code on each spring.
for (var springName in gW.aT.springMap) {
var spring = gW.aT.springMap[ springName];
var result = doThis( spring);
if (result && result['breakRequest']) break;
}
}
Spring.checkIfAttached = function ( objName) {
let yesFoundIt = false;
Spring.applyToAll( spring => {
if ( (!spring.forCursor) && ((spring.spo1.name == objName) || (spring.spo2.name == objName)) ) {
yesFoundIt = true;
return {'breakRequest':true};
}
});
return yesFoundIt;
}
Spring.deleteAll = function () {
// If any of these springs are b2d distance joints, remove these from the b2d world.
Spring.applyToAll( spring => {
if (spring.softConstraints) {
gW.world.DestroyJoint( spring.b2d);
spring.b2d = null;
}
});
gW.aT.springMap = {};
Spring.nameIndex = 0;
Spring.nameForPasting = null;
}
Spring.findAll_InMultiSelect = function ( doThis) {
// Find all the springs that have both ends (puck or pin) in the multi-select map.
// Then run the doThis function that has been passed in here.
Spring.countInMultiSelect = 0;
Spring.applyToAll( spring => {
if (spring.inMultiSelect()) {
Spring.countInMultiSelect++;
// For each spring you find there.
doThis( spring);
}
});
}
Spring.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
Spring.prototype.constructor = Spring; // Rename the constructor (after inheriting)
Spring.prototype.createDistanceJoint = function() {
var distance_joint = new b2DistanceJointDef;
// Identify the connected bodies.
distance_joint.bodyA = this.spo1.b2d;
distance_joint.bodyB = this.spo2.b2d;
// Connect to the attachment point on each body.
distance_joint.localAnchorA = b2Vec2_from_Vec2D( this.spo1_ap_l_2d_m);
distance_joint.localAnchorB = b2Vec2_from_Vec2D( this.spo2_ap_l_2d_m);
// Initialize the soft constraints.
distance_joint.length = this.length_m;
if ( ! this.fixedLength) {
distance_joint.frequencyHz = 1.0;
distance_joint.dampingRatio = 0.0;
}
// Will the connected bodies collide?
distance_joint.collideConnected = this.collideConnected;
// Add the joint to the world. And keep a reference to it here (this spring) as b2d.
this.b2d = gW.world.CreateJoint( distance_joint);
// Update it to reflect the traditional spring parameters and the effective mass.
this.updateB2D_spring();
}
Spring.prototype.updateB2D_spring = function() {
// Use the smaller of the two pucks in the frequency calculation.
var smallerMass_kg = 10000;
if (this.spo1.constructor.name == 'Puck') smallerMass_kg = Math.min(this.spo1.mass_kg, smallerMass_kg);
if (this.spo2.constructor.name == 'Puck') smallerMass_kg = Math.min(this.spo2.mass_kg, smallerMass_kg);
this.b2d.SetLength( this.length_m);
if ( ! this.fixedLength) {
// The frequency and damping ratio expressions are based on the equations on page 45 of this
// presentation by Erin Catto.
// https://www.timetocode.org/GDC2011_Catto_Erin_Soft_Constraints.pdf
// omega = (k/m)^0.5
// f = omega / 2Pi = (k/m)^0.5 / 2Pi
var freq_hz = Math.sqrt( this.strength_Npm/ smallerMass_kg)/(2.0 * Math.PI);
this.b2d.SetFrequency( freq_hz);
// dampingRatio = c / (2 * m * omega)
var dampingRatio = this.damper_Ns2pm2 / (2.0 * smallerMass_kg * (2.0 * Math.PI * this.b2d.GetFrequency()));
var dampingRatio_tweaked = dampingRatio /1.0 ;
this.b2d.SetDampingRatio( dampingRatio_tweaked);
}
}
Spring.prototype.deleteThisOne = function( pars) {
var deleteMode = setDefault( pars.deleteMode, null);
if (this.softConstraints) {
gW.world.DestroyJoint( this.b2d);
this.b2d = null;
}
if (this.navigationForNPC) {
// Dissociate the NPC puck from the navigation pin. Do this to prevent the
// navigation spring from regenerating when the capture is restored.
// Also disable the jet, since the NPC puck won't be motoring until attached to navigation again.
if (this.spo1.constructor.name == "Puck") {
this.spo1.pinName = null;
this.spo1.parsAtBirth.pinName = null;
this.spo1.disableJet = true;
}
if (this.spo2.constructor.name == "Puck") {
this.spo2.pinName = null;
this.spo2.parsAtBirth.pinName = null;
this.spo2.disableJet = true;
}
}
// Remove this spring from the spring map.
delete gW.aT.springMap[ this.name];
}
Spring.prototype.copyThisOne = function(p1, p2, copyMode) {
copyMode = setDefault( copyMode, "regular");
// 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;
// Update attributes that may have been changed (from birth) with editing.
pars.length_m = this.length_m;
pars.unstretched_width_m = this.unstretched_width_m
pars.strength_Npm = this.strength_Npm;
pars.damper_Ns2pm2 = this.damper_Ns2pm2;
// factor for drag force on attached pucks (proportional to velocity)
pars.drag_c = this.drag_c;
// Set local attachment points when pasting a spring.
if (copyMode == "pasteSingle") {
if (gW.dC.comSelection.checked) {
pars.spo1_ap_l_2d_m = new Vec2D(0,0);
pars.spo2_ap_l_2d_m = new Vec2D(0,0);
} else {
// Always paste onto the center of a pin.
pars.spo1_ap_l_2d_m = (p1.constructor.name == "Pin") ? new Vec2D(0,0) : p1.selectionPoint_l_2d_m;
pars.spo2_ap_l_2d_m = (p2.constructor.name == "Pin") ? new Vec2D(0,0) : p2.selectionPoint_l_2d_m;
}
}
// Note that this instantiation adds this new spring to the spring map.
var tempSpring = new Spring( p1, p2, pars);
// Also enable the jet for NPC pucks, since the NPC puck will be motoring now that it is attached to navigation again.
if (tempSpring.navigationForNPC) {
if (tempSpring.spo1.constructor.name == "Puck") {
tempSpring.spo1.disableJet = false;
}
if (tempSpring.spo2.constructor.name == "Puck") {
tempSpring.spo2.disableJet = false;
}
}
return tempSpring.name;
}
Spring.prototype.interpret_editCommand = function( command) {
var width_factor = 1.0;
var length_factor = 1.0;
var damping_factor = 1.0;
if ((command == 'wider') || (command == 'widerAppearance')) {
width_factor = 1.1;
} else if ((command == 'thinner') || (command == 'thinnerAppearance')) {
width_factor = 1.0/1.1;
} else if (command == 'taller') {
length_factor = 1.1;
} else if (command == 'shorter') {
length_factor = 1.0/1.1;
// For springs, interpret drag as damping.
} else if (command == 'moreDrag') {
damping_factor = 1.1;
} else if (command == 'lessDrag') {
damping_factor = 1.0/1.1;
} else if (command == 'noChange') {
// don't change anything.
}
// First, the special case of the pinned puck that is using a zero length spring. Give
// it a little length to start with, otherwise the zero will always scale to zero (it will never
// get longer).
if (command=='shorter' || command=='taller') {
if (this.length_m == 0.0) this.length_m = 0.1;
this.length_m *= length_factor;
if (this.length_m < 0.1) this.length_m = 0.0;
gW.messages['help'].newMessage("spring: [base,yellow]" + this.name + "[base] length = " + this.length_m.toFixed(3), 0.5);
} else if (command=='thinner' || command=='wider') {
// Use the wider/thinner width_factor to affect both the visual width and strength of the spring.
// See below, in the draw method, that width_px, the drawing width, has a minimum of 2px.
this.unstretched_width_m *= width_factor;
this.strength_Npm *= width_factor;
gW.messages['help'].newMessage("spring: [base,yellow]" + this.name + "[base] strength (k) = " + this.strength_Npm.toFixed(4), 0.5);
} else if (command=='thinnerAppearance' || command=='widerAppearance') {
// Use the widerAppearance/thinnerAppearance width_factor to affect ONLY the visual width of the spring.
// (see comment above on width_px)
this.unstretched_width_m *= width_factor;
gW.messages['help'].newMessage("spring: [base,yellow]" + this.name + "[base] unstretched width = " + this.unstretched_width_m.toFixed(4), 0.5);
} else if (command=='lessDrag' || command=='moreDrag') {
// If at zero, give the scaling factor something to work with.
if (this.damper_Ns2pm2 == 0.0) this.damper_Ns2pm2 = 0.1;
// Apply the scaling factor.
this.damper_Ns2pm2 *= damping_factor;
// A lower limit.
if (this.damper_Ns2pm2 < 0.1) this.damper_Ns2pm2 = 0.0;
gW.messages['help'].newMessage("spring: [base,yellow]" + this.name + "[base] damping = " + this.damper_Ns2pm2.toFixed(4), 0.5);
}
// If you're using a distance joint in Box2D...
if (this.softConstraints) {
this.updateB2D_spring();
}
}
Spring.prototype.updateEndPoints = function() {
// Find the world position of the attachment points.
// if not attached to the center
if ( ! this.spo1_ap_l_2d_m.zeroLength() ) {
this.spo1_ap_w_2d_m = Vec2D_from_b2Vec2( this.spo1.b2d.GetWorldPoint( this.spo1_ap_l_2d_m));
// if attached to the center
} else {
this.spo1_ap_w_2d_m = this.spo1.position_2d_m;
}
this.spo1_ap_w_2d_px = screenFromWorld( this.spo1_ap_w_2d_m);
if ( ! this.spo2_ap_l_2d_m.zeroLength() ) {
this.spo2_ap_w_2d_m = Vec2D_from_b2Vec2( this.spo2.b2d.GetWorldPoint( this.spo2_ap_l_2d_m));
} else {
this.spo2_ap_w_2d_m = this.spo2.position_2d_m;
}
this.spo2_ap_w_2d_px = screenFromWorld( this.spo2_ap_w_2d_m);
}
Spring.prototype.calculateSeparation = function() {
this.updateEndPoints();
// Separation vector and its length:
// Need these two results for both distance joints and regular springs:
this.p1p2_separation_2d_m = this.spo1_ap_w_2d_m.subtract( this.spo2_ap_w_2d_m);
this.p1p2_separation_m = this.p1p2_separation_2d_m.length();
this.p1p2_normalized_2d = this.p1p2_separation_2d_m.scaleBy( 1/this.p1p2_separation_m);
this.stretch_m = (this.p1p2_separation_m - this.length_m);
}
Spring.prototype.force_on_pucks = function() {
/*
If springs are modeled with Hooke's law, determine all the forces
(related to the spring) that act on the two attached bodies. This
includes forces acting at the attachment points and those acting at the
COMs. Calculate:
-- separation distance (length) and vector between the two attachment points for calculating the spring forces
-- relative speed of the attachment points for use in calculating the damping forces
-- absolute speed of each attachment point for use in calculating drag forces
Some of this is also needed for drawing the springs modeled as distance
joints (in Box2D engine).
*/
this.calculateSeparation();
// If not using the native spring modeling (distance joints) in b2d, calculate the spring and damping forces.
// note: the logical operator forces a type conversion if softConstraints is undefined (converts to false, so !false is true)
if ( ! this.softConstraints) {
/*
First, calculate the forces that don't necessarily act on the center of the body, non COM.
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.
The second clause in this if statement checks for use of the editor,
the control key. Block cursor-spring forces when doing deterministic
movements. This only blocks traditional springs. If in distance-joint
mode, the cursor movement will drag the selected puck some (a little)
even when control key is down (and using shift or alt keys for
rotation).
*/
if ( ((this.p1p2_separation_m == 0.0) && (this.length_m == 0.0)) || ((this.forCursor && gW.clients[this.name].key_ctrl == "D")) ) {
var spring_force_on_1_2d_N = new Vec2D(0.0,0.0);
} else {
// Spring force: acts along the separation vector and is proportional to the stretch distance.
var spring_force_on_1_2d_N = this.p1p2_normalized_2d.scaleBy( -this.stretch_m * this.strength_Npm);
}
/*
These non-COM spring forces must be applied individually, at the
attachment points. That's why these are appended to the puck's
nonCOM_2d_N force array. This array is reset (emptied) after the
movements are calculated in the physics engine.
*/
if (this.spo1.constructor.name == "Puck") {
this.spo1.nonCOM_2d_N.push({'force_2d_N': spring_force_on_1_2d_N.scaleBy( +1), 'point_w_2d_m': this.spo1_ap_w_2d_m});
/*
The following vector is used for aiming the NPC's navigation jets. (Note
navigation springs are always conventional springs.) Check to see that
this is on a navigation pin before updating navSpringOnly_force_2d_N. We
only want the navigation spring force to be affecting the drawing of the
navigation jet. This will exclude other springs, like cursor springs,
from affecting the jet representation.
*/
if ((this.spo2.constructor.name == "Pin") && (this.spo2.NPC)) this.spo1.navSpringOnly_force_2d_N = spring_force_on_1_2d_N.scaleBy( +1);
}
if (this.spo2.constructor.name == "Puck") {
this.spo2.nonCOM_2d_N.push({'force_2d_N': spring_force_on_1_2d_N.scaleBy( -1), 'point_w_2d_m': this.spo2_ap_w_2d_m});
// (see explanation in spo1 block above)
if ((this.spo1.constructor.name == "Pin") && (this.spo1.NPC)) this.spo2.navSpringOnly_force_2d_N = spring_force_on_1_2d_N.scaleBy( -1);
}
// Damper force: acts along the separation vector and is proportional to the relative speed.
// First, get the velocity at each attachment point.
var v_spo1_ap_2d_mps = Vec2D_from_b2Vec2( this.spo1.b2d.GetLinearVelocityFromWorldPoint( this.spo1_ap_w_2d_m));
var v_spo2_ap_2d_mps = Vec2D_from_b2Vec2( this.spo2.b2d.GetLinearVelocityFromWorldPoint( this.spo2_ap_w_2d_m));
var v_relative_2d_mps = v_spo1_ap_2d_mps.subtract( v_spo2_ap_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);
// This damper force acts in opposite directions for each of the two pucks.
if (this.spo1.constructor.name == "Puck") {
// Again, notice the negative sign here, opposite to the spring force.
this.spo1.nonCOM_2d_N.push({'force_2d_N': damper_force_on_1_2d_N.scaleBy( -1), 'point_w_2d_m': this.spo1_ap_w_2d_m});
}
if (this.spo2.constructor.name == "Puck") {
this.spo2.nonCOM_2d_N.push({'force_2d_N': damper_force_on_1_2d_N.scaleBy( +1), 'point_w_2d_m': this.spo2_ap_w_2d_m});
}
}
/*
The following drag forces act at the puck's COM.
These forces are not calculated for the b2d distance joints. So,
need these in order to reproduce the behavior of the old cursor strings
(now springs). These are based on the velocity of the pucks (not
relative speed as is the case above for damper forces).
This adds to (vector add using addTo) the puck's sprDamp_force_2d_N
vector. By the time you've looped through all the springs, you get the
NET damping force, on each puck COM, applied by all the individual springs.
This aggregate is reset (zeroed) after the movements are calculated.
*/
if (this.spo1.constructor.name == "Puck") {
this.spo1.sprDamp_force_2d_N.addTo( this.spo1.velocity_2d_mps.scaleBy( -1 * this.drag_c));
}
if (this.spo2.constructor.name == "Puck") {
this.spo2.sprDamp_force_2d_N.addTo( this.spo2.velocity_2d_mps.scaleBy( -1 * this.drag_c));
}
}
Spring.prototype.inMultiSelect = function() {
if ((this.spo1.name in gW.hostMSelect.map) && (this.spo2.name in gW.hostMSelect.map)) {
return true;
} else {
return false;
}
}
Spring.prototype.report = function( ) {
let stretchValue, lengthPhrase, dragPhrase, springNature;
if ( ! this.fixedLength) {
// Getting a little fancy here: putting a sign (+-) a space away from the absolute value of the stretch.
if (this.stretch_m > 0) {
stretchValue = " [base]+ " + this.stretch_m.toFixed(2) + "[base]";
} else {
// − is the code for a minus sign. A true minus sign is better than a dash from keyboard (which is wider than a plus sign).
stretchValue = " [base,cyan]" + String.fromCharCode(8722) + " " + Math.abs(this.stretch_m).toFixed(2) + "[base]";
}
lengthPhrase = "\\ length + stretch : " + this.length_m.toFixed(2) + stretchValue + " = " + (this.length_m + this.stretch_m).toFixed(2);
// This spring is modeled as Hooke's if this.softConstraints is undefined or false.
springNature = (this.softConstraints === true) ? "soft constraint" : "Hooke's Law";
} else {
lengthPhrase = "\\ length = " + this.length_m.toFixed(2);
springNature = "fixed length";
}
dragPhrase = (this.drag_c > 0) ? ", drag = " + this.drag_c.toFixed(2) : "";
let objReport = "spring: [base,yellow]" + this.name + "[base]" + " (" + springNature + ")" +
"\\ k = " + this.strength_Npm.toFixed(2) + ", unstretched width = " + this.unstretched_width_m.toFixed(3) +
lengthPhrase +
"\\ damping = " + this.damper_Ns2pm2.toFixed(2) + dragPhrase;
if (gW.getDemoVersion().slice(0,3) == "5.c") {
let pendulumPeriod = 2 * Math.PI * Math.sqrt( (this.length_m + this.stretch_m) / gW.getG_mps2() );
let springPeriod = 2 * Math.PI * Math.sqrt( gW.aT.puckMap['puck2'].mass_kg / this.strength_Npm );
let ratio_PS = pendulumPeriod / springPeriod;
objReport+=
"\\ \\pendulum T = " + pendulumPeriod.toFixed(1) + " s" +
"\\spring T = " + springPeriod.toFixed(1) + " s" +
"\\ratio = " + ratio_PS.toFixed(2);
}
gW.messages['help2'].newMessage( objReport, 0.05);
}
Spring.prototype.draw = function( drawingContext) {
let alpha = 0.7, width_m;
// Update endpoints and the normal vector calculated from them...
this.calculateSeparation();
if ((this.navigationForNPC && gW.dC.editor.checked) || (!this.visible && gW.dC.editor.checked) || (this.visible && !this.navigationForNPC)) {
/*
// These two width calculations will cause some discontinuity in how the springs look if they are being
// length adjusted between zero and non-zero, especially for a puck in gravity on a zero-length spring. It's a compromise.
if (this.length_m == 0) {
// This version looks better for zero-length (pinned pucks)
var width_m = this.unstretched_width_m * (1 - (0.40 * this.p1p2_separation_m));
} else {
// This version of the width calculation conserves the area of the spring.
var width_m = (this.unstretched_width_m * this.length_m) / this.p1p2_separation_m;
}
*/
/* This is one way to deal with zero-length springs in the width
calculation that follow and does not produce the discontinuity of the
old method (above). The length_min_m effectively gives the spring some
cross-sectional area to stretch, and calculate a non-zero width. You may
still see some less-than-ideal behavior for a zero-length spring in
gravity as the length is dynamically increased from zero. */
let length_min_m = (this.length_m < 0.6) ? 0.6 : this.length_m; // 0.8
// protect from divide-by-zero
let separation_min_m = (this.p1p2_separation_m < 0.01) ? 0.01 : this.p1p2_separation_m;
// conservation of area: unstretched_width * length = width * (length + stretch)
width_m = this.unstretched_width_m * length_min_m / separation_min_m;
// conservation of volume: unstretched_width^2 * length = width^2 * (length + stretch)
//width_m = Math.sqrt( Math.pow( this.unstretched_width_m, 2) * length_min_m / separation_min_m);
// note: also played with this non-conserving width calculation
//let width_m = this.unstretched_width_m * (1 - 0.25 * (this.stretch_m / length_min_m));
// Prevent the width value from getting too large.
if (width_m > (3 * this.unstretched_width_m)) {
width_m = 3 * this.unstretched_width_m;
}
var width_px = px_from_meters( width_m);
if (width_px < 2) width_px = 2;
var fillColor = (drawingContext.globalCompositeOperation == 'screen') ? 'white' : this.color;
if (this.selected) {
var dashArray = [3];
// Must use the default 'butt' ends if the lines are dashed.
// Note: dashed lines require surprising CPU drain.
var lineCap = 'butt';
} else {
if (this.name == Spring.nameForPasting) {
var dashArray = [5,1]; // pattern: 5px solid, 1px gap, ...
var lineCap = 'butt';
alpha = 1.0; // source spring for pasting will render brighter.
} else if (this.name == gW.hostMSelect.candidateReportPasteDelete) {
var dashArray = [5];
var lineCap = 'butt';
fillColor = 'white';
alpha = 1.0;
} else {
// If not dashed, you can use the fancy 'round' ends. Nice.
var dashArray = [0];
var lineCap = 'round';
}
}
if ( ! gW.getPauseErase()) {
// This spring may be a cursor spring (this.forCursor is true). In that case, the name of the spring is also the client name. So
// you'll see references to this.forCursor followed by references to the client in the clients map using the spring's name.
// Draw the spring.
if (this.spo1_ap_w_2d_px && this.spo2_ap_w_2d_px) {
this.drawLine( drawingContext, this.spo1_ap_w_2d_px, this.spo2_ap_w_2d_px,
{'width_px':width_px, 'color':fillColor, 'dashArray':dashArray, 'alpha':alpha, 'lineCap':lineCap} );
// small circle at each end
this.drawCircle( drawingContext, this.spo1_ap_w_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'lightgray', 'radius_px':3});
this.drawCircle( drawingContext, this.spo2_ap_w_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'lightgray', 'radius_px':3});
// draw extension line for monkeyhunt...
if ( gW.getDemoVersion().includes('monkeyhunt') &&
(this.forCursor) && (gW.clients[this.name].ctrlShiftLock) && (gW.clients[this.name].selectedBody.constructor.name == "Puck") && (gW.clients[this.name].key_ctrl != "D") &&
( ! gW.clients[this.name].compoundBodySelected()) ) {
let extensionPoint_2d_m = this.spo1_ap_w_2d_m.add( this.p1p2_normalized_2d.scaleBy( 20.0));
let extensionPoint_2d_px = screenFromWorld( extensionPoint_2d_m);
this.drawLine( drawingContext, this.spo1_ap_w_2d_px, extensionPoint_2d_px,
{'width_px':2, 'color':'white', 'dashArray':[3], 'alpha':0.5, 'lineCap':lineCap} );
}
}
// Draw a line to indicate the locked-speed value for puck shots.
var readyToShoot = (this.forCursor) && (((gW.clients[this.name].key_ctrl == "D") && (gW.clients[this.name].key_shift == "D")) || (gW.clients[this.name].ctrlShiftLock));
if ((readyToShoot) && (gW.clients[this.name].poolShotLocked)) {
// normal vector along the spring.
if ( ! this.p1p2_separation_2d_m.zeroLength()) {
// 1/2*m*v^2 = 1/2*k*x^2
var energyAtLockSpeed_J = 0.5 * gW.clients[this.name].selectedBody.mass_kg * Math.pow( gW.clients[this.name].poolShotLockedSpeed_mps, 2);
var stretchAtLockSpeed_m = Math.pow( 2 * energyAtLockSpeed_J / this.strength_Npm, 0.5);
// Don't extend the indicator line past the ghost ball (ghost must be separated enough to show the speed lock segment).
if (stretchAtLockSpeed_m < this.p1p2_separation_m) {
var endPoint_2d_m = this.p1p2_normalized_2d.scaleBy( stretchAtLockSpeed_m).add( this.spo2_ap_w_2d_m);
} else {
var endPoint_2d_m = this.spo1_ap_w_2d_m;
}
var endPoint_2d_px = screenFromWorld( endPoint_2d_m);
// For the yellow client, shift to a red indicator.
var indicatorColor = (fillColor == 'yellow') ? 'red':'yellow';
// Draw line from the source ball (object 2) out to the endpoint, toward the cursor and the ghost ball (object 1).
this.drawLine( drawingContext, this.spo2_ap_w_2d_px, endPoint_2d_px,
{'width_px':width_px * 2.0, 'color':indicatorColor, 'dashArray':dashArray, 'alpha':alpha, 'lineCap':lineCap} );
}
}
}
}
}
function Wall( position_2d_m, pars) {
DrawingFunctions.call(this); // inherit
this.parsAtBirth = pars;
//this.alsoThese = [];
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)));
} else {
Wall.nameIndex += 1;
this.name = 'wall' + Wall.nameIndex;
}
gW.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 = setDefault( pars.fence, false);
this.fenceLeg = setDefault( pars.fenceLeg, null);
if (this.fenceLeg == 'top') Wall.topFenceLegName = this.name; // For use in piCalcEngine
this.sensor = setDefault( pars.sensor, false);
this.visible = setDefault( pars.visible, true);
this.velocity_2d_mps = setDefault( pars.velocity_2d_mps, new Vec2D(0.0, 0.0));
// Make sure this is a Vec2D vector (e.g. when restoring from a capture).
this.velocity_2d_mps = Vec2D_from_b2Vec2( this.velocity_2d_mps);
this.angle_r = setDefault( pars.angle_r, 0.0);
this.angularSpeed_rps = setDefault( pars.angularSpeed_rps, 0.0);
// Dimensions (as specified in box2D)
this.half_width_m = setDefault( pars.half_width_m , 0.5);
this.half_height_m = setDefault( 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);
Wall.color_default = "darkgray"; // white
this.color = setDefault( pars.color, Wall.color_default);
// Local selection point where candidate revolute joints can be attached.
this.selectionPoint_l_2d_m = new Vec2D(0,0);
this.deleted = false;
this.monkeyHunt = setDefault( pars.monkeyHunt, false);
// All parameters should be set above this point.
this.b2d = null;
this.create_b2d_wall();
// Create a reference back to this wall from the b2d wall.
gW.tableMap.set(this.b2d, this);
}
Wall.nameIndex = 0;
Wall.topFenceLegName = null; // For use in piCalcEngine
Wall.applyToAll = function( doThis) {
for (var wallName in gW.aT.wallMap) {
//console.log("name in applyToAll="+wallName);
var wall = gW.aT.wallMap[ wallName];
doThis( wall);
}
}
Wall.deleteAll = function() {
Wall.applyToAll( wall => {
gW.tableMap.delete( wall.b2d);
if (wall.b2d) gW.world.DestroyBody( wall.b2d);
});
gW.aT.wallMap = {};
Wall.nameIndex = 0;
}
Wall.makeFence = function( pars = {}, canvas) {
// 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;
// By default, all four walls of the fence are generated.
var tOn = setDefault( pars.tOn, true);
var bOn = setDefault( pars.bOn, true);
var lOn = setDefault( pars.lOn, true);
var rOn = setDefault( pars.rOn, true);
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
if (tOn) new Wall( new Vec2D( half_width_m, height_m - pull_in_m), Object.assign({'fenceLeg':'top'}, short_wide_dimensions) );
// bottom
if (bOn) new Wall( new Vec2D( half_width_m, 0.00 + pull_in_m), Object.assign({'fenceLeg':'bottom'}, short_wide_dimensions) );
// left
if (lOn) new Wall( new Vec2D( 0.00 + pull_in_m, half_height_m), Object.assign({'fenceLeg':'left'}, tall_skinny_dimensions) );
// right
if (rOn) new Wall( new Vec2D( width_m - pull_in_m, half_height_m), Object.assign({'fenceLeg':'right'}, tall_skinny_dimensions) );
}
Wall.deleteFence = function() {
Wall.applyToAll( wall => {
if (wall.fence) {
wall.deleteThisOne({});
}
});
}
Wall.deleteAllButFence = function() {
Wall.applyToAll( wall => {
if ( ! wall.fence) {
wall.deleteThisOne({});
}
});
}
Wall.checkForFence = function() {
let foundFence = false;
Wall.applyToAll( wall => {
if (wall.fence) {
foundFence = true;
}
});
return foundFence;
}
Wall.getFenceParms = function() {
let fenceParms = {'bOn':false, 'tOn':false, 'rOn':false, 'lOn':false};
Wall.applyToAll( wall => {
if (wall.fence) {
if (wall.fenceLeg) {
if (wall.fenceLeg == "top") {
fenceParms['tOn'] = true;
} else if (wall.fenceLeg == "bottom") {
fenceParms['bOn'] = true;
} else if (wall.fenceLeg == "right") {
fenceParms['rOn'] = true;
} else if (wall.fenceLeg == "left") {
fenceParms['lOn'] = true;
}
} else {
// Try to identify the section of fence by its location.
if (wall.position_2d_m.x == 0) {
fenceParms['lOn'] = true;
} else if (wall.position_2d_m.y == 0) {
fenceParms['bOn'] = true;
} else if (wall.position_2d_m.y > wall.position_2d_m.x) {
fenceParms['tOn'] = true;
} else if (wall.position_2d_m.x > wall.position_2d_m.y) {
fenceParms['rOn'] = true;
}
}
}
});
return fenceParms;
}
Wall.prototype = Object.create( DrawingFunctions.prototype); // Inherit methods
Wall.prototype.constructor = Wall; // Rename the constructor (after inheriting)
Wall.prototype.deleteThisOne = function( pars) {
var deleteMode = setDefault( pars.deleteMode, null);
// Delete reference in the tableMap.
gW.tableMap.delete( this.b2d);
// Delete the corresponding Box2d object.
gW.world.DestroyBody( this.b2d);
// Mark this wall as deleted. Therefore, any springs or joints that
// have a reference to this wall will be removed in the game loop.
this.deleted = true;
// Remove this wall from the wall map.
delete gW.aT.wallMap[ this.name];
// ...and from the multi-select map.
gW.hostMSelect.removeOne( this);
}
Wall.prototype.copyThisOne = function( pars) {
var position_2d_m = setDefault( pars.position_2d_m, this.position_2d_m);
return new Wall( 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,
'monkeyHunt':this.monkeyHunt});
}
Wall.prototype.define_fixture = function( pars) {
// Note that the default behavior is to have all scaling factors at 1.0 which only updates the box2d attributes
// to correspond to those of the Wall object.
this.width_scaling = setDefault( pars.width_scaling, 1.0);
this.height_scaling = setDefault( pars.height_scaling, 1.0);
var fixDef = new b2FixtureDef;
fixDef.shape = new b2PolygonShape;
if (this.sensor) fixDef.isSensor = true;
// 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);
return fixDef;
}
Wall.prototype.create_b2d_wall = function() {
// Create a rectangular box2d object.
var bodyDef = new b2BodyDef;
// When initially tried the b2_kinematicBody type, had trouble with collisions after using the mouse to
// rotate the wall. Collisions (post wall rotation) would clear off the table, delete all pucks.
// So played around with animating (see updateStaticBodyState method below) a static body, to avoid this issue.
// But of course, kinematic bodies can have their movement represented in the engine and so can model
// collisions with things like a moving paddle. So the kinematic body is really needed if the walls are
// going to be rotating. Also see the key_t block of the keydown event handler in gwModule.
bodyDef.type = b2Body.b2_kinematicBody; // b2_kinematicBody b2_staticBody
this.b2d = gW.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.SetAngle( this.angle_r);
if (this.b2d.m_type != b2Body.b2_staticBody) {
this.b2d.SetLinearVelocity( this.velocity_2d_mps);
this.b2d.SetAngularVelocity( this.angularSpeed_rps);
}
}
Wall.prototype.setPosition = function( newPosition_2d_m, angle_r = 0.0) {
this.position_2d_m = newPosition_2d_m;
this.b2d.SetPosition( newPosition_2d_m);
this.angle_r = angle_r;
this.b2d.SetAngle( this.angle_r);
}
Wall.prototype.setVelocity = function( newVelocity_2d_mps) {
if ( ! this.b2d.IsAwake()) this.b2d.SetAwake( true);
this.velocity_2d_mps = newVelocity_2d_mps;
this.b2d.SetLinearVelocity( newVelocity_2d_mps);
}
Wall.prototype.interpret_editCommand = function( command) {
// 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 (command == 'wider') {
width_factor = 1.1;
} else if (command == 'thinner') {
width_factor = 1.0/1.1;
} else if (command == 'taller') {
height_factor = 1.1;
} else if (command == 'shorter') {
height_factor = 1.0/1.1;
} else if (command == 'noChange') {
// don't change anything.
}
this.b2d.DestroyFixture( this.b2d.m_fixtureList);
this.b2d.CreateFixture( this.define_fixture({'width_scaling':width_factor,'height_scaling':height_factor}));
var dimensionsReport = "half width, half height = " + this.half_width_m.toFixed(3) + ", " + this.half_height_m.toFixed(3) + " m";
gW.messages['help'].newMessage( dimensionsReport, 1.0);
}
Wall.prototype.draw_MultiSelectPoint = function( drawingContext) {
var selectionPoint_2d_px;
if ( ! gW.dC.comSelection.checked) {
selectionPoint_2d_px = screenFromWorld( this.b2d.GetWorldPoint( this.selectionPoint_l_2d_m));
} else {
selectionPoint_2d_px = this.position_2d_px;
}
this.drawCircle( drawingContext, selectionPoint_2d_px, {'borderColor':'black', 'borderWidth_px':1, 'fillColor':'yellow', 'radius_px':5});
}
Wall.prototype.getPosition = function() {
this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition());
this.position_2d_px = screenFromWorld( this.position_2d_m);
}
Wall.prototype.updateStaticBodyState = function() {
// If modeling walls with static box2d bodies, must animate them directly. Compare
// with pins, which always use kinematic bodies; they don't have animation code like this.
// (see comment in create_b2d_wall)
if (this.b2d.m_type == b2Body.b2_staticBody) {
this.position_2d_m = Vec2D_from_b2Vec2( this.b2d.GetPosition());
this.position_2d_px = screenFromWorld( this.position_2d_m);
this.position_2d_m.addTo( this.velocity_2d_mps.scaleBy( gW.getDeltaT_s()));
this.b2d.SetPosition( b2Vec2_from_Vec2D( this.position_2d_m));
this.angle_r += this.angularSpeed_rps * gW.getDeltaT_s();
this.b2d.SetAngle( this.angle_r);
}
}
Wall.prototype.draw = function( drawingContext) {
// (see comment in create_b2d_wall)
// Update the position of the wall's center point and it's body angle.
if (this.b2d.m_type == b2Body.b2_kinematicBody) {
this.angle_r = this.b2d.GetAngle();
this.getPosition();
}
// However, directly use the vertices of the rectangle for drawing.
if (this.visible) {
this.drawPolygon( drawingContext, gW.b2d_getPolygonVertices_2d_px( 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) {
// Only process good stuff.
if (new_value && isFinite( 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;
} else {
return new_value;
}
}
function SoundEffect( filePath, nCopies, volumeBase = 1.00) {
// The copies allow the same sound to be called repeatedly and thereby played in overlapping sequences.
// This is useful for the clack sound in the calculating pi demos.
this.copies = [];
this.nCopies = nCopies;
this.index = 0;
this.volumeBase = volumeBase;
for (var i = 0; i < nCopies; i++) {
this.copies.push(new Audio( filePath));
}
}
SoundEffect.prototype.play = function( volumeChangeFraction = 1.00) {
let oneSound = this.copies[this.index];
oneSound.volume = this.volumeBase * volumeChangeFraction;
if (oneSound.volume < 0) oneSound.volume = 0;
if (oneSound.volume > 1) oneSound.volume = 1;
// A demo the plays directly from a URL, will not play sound until the user clicks somewhere.
// So the error message is translated to the following advice.
var playPromise = oneSound.play();
playPromise.then( function() {
}).catch( function(error) {
console.log(error.name + ': ' + error.message);
if (error.name == 'NotAllowedError') {
gW.messages['help2'].newMessage('Sound effects are disabled until you type or click.\\ Go ahead; interact.', 2.0);
}
});
if (this.index < this.nCopies - 1) {
this.index++;
} else {
this.index = 0;
}
}
function PiEngine( puck1, puck2, clackSound, pars = {} ) {
// A 1D engine for calculating the digits of pi by counting the collisions of
// of two pucks. Pucks must have a mass ratio of 100^(d-1), where d is the number
// of digits of pi to be determined. Smaller puck is between the wall and the larger puck.
this.p1 = puck1;
this.p2 = puck2;
this.clackSound = clackSound;
// 1000 works well for up to 5 digits of pi.
this.nFinerTimeStepFactor = setDefault( pars.nFinerTimeStepFactor, 1000);
this.lastCollidedWithWall = setDefault( pars.lastCollidedWithWall, true);
this.clacks = setDefault( pars.clacks, true);
this.reportsEnabled = setDefault( pars.enabled, true);
this.atLeastOneCollisionInFrame = setDefault( pars.atLeastOneCollisionInFrame, false);
this.p1_v_max = setDefault( pars.p1_v_max, 0);
this.collisionCount = setDefault( pars.collisionCount, 0);
}
// This state object is used in the capture and restore process...
PiEngine.state = {};
PiEngine.prototype.step = function( dt_oneFrame_s) {
this.dt_s = dt_oneFrame_s / this.nFinerTimeStepFactor;
this.atLeastOneCollisionInFrame = false;
for (var i=0; i < this.nFinerTimeStepFactor; i++) {
this.update( this.p1);
this.update( this.p2);
this.checkForCollisions();
}
if (this.atLeastOneCollisionInFrame) {
if (this.clacks) this.clackSound.play();
if (this.reportsEnabled) this.report();
}
}
PiEngine.prototype.update = function( puck) {
puck.position_2d_m.y += puck.velocity_2d_mps.y * this.dt_s;
// Since not using the Box2D engine, manually update the pucks in the Box2D world.
puck.b2d.SetPosition( puck.position_2d_m);
}
PiEngine.prototype.checkForCollisions = function() {
// Note that the lastCollidedWithWall logical mandates alternation between wall and puck collisions.
// Check pucks for puck-puck collisions
if (((this.p1.position_2d_m.y - this.p1.radius_m) < (this.p2.position_2d_m.y + this.p2.radius_m)) && (this.lastCollidedWithWall)) {
this.lastCollidedWithWall = false;
this.puckCollisionResult();
this.countit();
}
// Check puck1 for collisions with the top leg of the fence.
var botEdgeTopWall_y_m = gW.aT.wallMap[ Wall.topFenceLegName].position_2d_m.y - gW.aT.wallMap[ Wall.topFenceLegName].half_height_m;
if ( ((this.p1.position_2d_m.y + this.p1.radius_m) > botEdgeTopWall_y_m) && (!this.lastCollidedWithWall) ) {
this.lastCollidedWithWall = true;
this.p1.velocity_2d_mps.y *= -1.0;
this.countit();
}
}
PiEngine.prototype.countit = function() {
this.atLeastOneCollisionInFrame = true;
this.collisionCount += 1;
}
PiEngine.prototype.report = function() {
gW.messages['help'].newMessage("count = " + this.collisionCount + "\\v max = " + this.p1_v_max.toFixed(1));
}
PiEngine.prototype.puckCollisionResult = function() {
var CR = 1.0;
var p1_v_y = ( (CR * this.p2.mass_kg * (this.p2.velocity_2d_mps.y - this.p1.velocity_2d_mps.y) +
this.p1.mass_kg * this.p1.velocity_2d_mps.y +
this.p2.mass_kg * this.p2.velocity_2d_mps.y) / (this.p1.mass_kg + this.p2.mass_kg) );
var p2_v_y = ( (CR * this.p1.mass_kg * (this.p1.velocity_2d_mps.y - this.p2.velocity_2d_mps.y) +
this.p1.mass_kg * this.p1.velocity_2d_mps.y +
this.p2.mass_kg * this.p2.velocity_2d_mps.y) / (this.p1.mass_kg + this.p2.mass_kg) );
this.p1_v_max = Math.max( p1_v_y, this.p1_v_max);
this.p1.velocity_2d_mps.y = p1_v_y;
this.p2.velocity_2d_mps.y = p2_v_y;
}
function DrawingFunctions(){
// High-level functions for drawing to the canvas element
}
DrawingFunctions.prototype.drawLine = function( drawingContext, p1_2d_px, p2_2d_px, pars) {
drawingContext.strokeStyle = setDefault( pars.color, 'white');
drawingContext.lineWidth = setDefault( pars.width_px, 2);
var dashArray = setDefault( pars.dashArray, [0]);
var alpha = setDefault( pars.alpha, 1.0);
var lineCap = setDefault( pars.lineCap, 'butt');
drawingContext.globalAlpha = alpha;
drawingContext.setLineDash( dashArray);
drawingContext.lineCap = lineCap;
drawingContext.beginPath();
drawingContext.moveTo(p1_2d_px.x, p1_2d_px.y);
drawingContext.lineTo(p2_2d_px.x, p2_2d_px.y);
drawingContext.stroke();
drawingContext.globalAlpha = 1.0;
drawingContext.lineCap = 'butt';
}
DrawingFunctions.prototype.drawCircle = function( drawingContext, center_2d_px, pars) {
drawingContext.strokeStyle = setDefault( pars.borderColor, 'white');
drawingContext.lineWidth = setDefault( pars.borderWidth_px, 2);
var radius_px = setDefault( pars.radius_px, 6);
var fillColor = setDefault( pars.fillColor, 'red');
var fillAlpha = setDefault( pars.fillAlpha, 1.00);
var lineAlpha = setDefault( pars.lineAlpha, 1.00);
var dashArray = setDefault( pars.dashArray, [0]);
drawingContext.setLineDash( dashArray);
drawingContext.beginPath();
drawingContext.arc( center_2d_px.x, center_2d_px.y, radius_px, 0, 2 * Math.PI);
// Note that specifying an alpha of 0.0 is equivalent to a fillColor of 'noFill'.
if (fillColor != 'noFill') {
drawingContext.globalAlpha = fillAlpha;
drawingContext.fillStyle = fillColor;
drawingContext.fill();
drawingContext.globalAlpha = 1.00;
}
if (pars.borderWidth_px > 0) {
drawingContext.globalAlpha = lineAlpha;
drawingContext.stroke();
drawingContext.globalAlpha = 1.00;
}
// Turn off the dashes
drawingContext.setLineDash([0]);
}
DrawingFunctions.prototype.drawPolygon = function( drawingContext, poly_px, pars) {
var borderWidth_px = setDefault( pars.borderWidth_px, 2);
var lineAlpha = setDefault( pars.lineAlpha, 1.00);
// drawingContext.lineWidth will revert to default or previous value if you attempt to set it to zero.
if (borderWidth_px > 0) {
drawingContext.lineWidth = borderWidth_px;
} else {
drawingContext.lineWidth = 1;
}
drawingContext.fillStyle = setDefault( pars.fillColor, 'red');
var fillIt = setDefault( pars.fillIt, true);
// Hide the border, using a color match, if zero width.
if (borderWidth_px > 0) {
drawingContext.strokeStyle = setDefault( pars.borderColor, 'white');
} else {
drawingContext.strokeStyle = drawingContext.fillStyle;
}
drawingContext.setLineDash([0]);
drawingContext.beginPath();
drawingContext.moveTo( poly_px[0].x, poly_px[0].y);
for (var i = 1, len = poly_px.length; i < len; i++) {
drawingContext.lineTo( poly_px[i].x, poly_px[i].y);
}
drawingContext.closePath();
if (fillIt) drawingContext.fill();
drawingContext.globalAlpha = lineAlpha;
drawingContext.stroke();
drawingContext.globalAlpha = 1.00;
}
DrawingFunctions.prototype.fillRectangle = function( drawingContext, upperLeft_2d_px, pars) {
// Draw solid rectangle.
var width_px = setDefault( pars.width_px, 6);
var height_px = setDefault( pars.height_px, width_px); // default is square
drawingContext.fillStyle = setDefault( pars.fillColor, 'red');
// -----------upper left corner--------
drawingContext.fillRect( upperLeft_2d_px.x, upperLeft_2d_px.y, width_px, height_px);
}
// Reveal public references.
return {
// Objects (prototypes)
Vec2D: Vec2D,
Client: Client,
Puck: Puck,
Joint: Joint,
Spring: Spring,
Wall: Wall,
Pin: Pin,
HelpMessage: HelpMessage,
MultiSelect: MultiSelect,
SelectBox: SelectBox,
RunningAverage: RunningAverage,
SoundEffect: SoundEffect,
PiEngine: PiEngine,
DrawingFunctions: DrawingFunctions
};
})();