/* This module represent one layer of many possible layers that could be at the Floating Space. Objects in the same layer are subject to the same physics, and by being at the same level, they interact with each other. For example, they can bounce when one hit the other. This module posesses its own physics engine. There are different types of floating objects, but this layer takes care of each of them. Floating Objects Types: 1. Profile Balls: These are small balls with a profile picture, and two levels, one above and one below the ball. They are usually used to specify the location of a bot instance. 2. Notes: Notes are a rectangular area where some text is posted. It has a subject and a body. 3. UI Objects: These are small balls that represent parts of an strategy. */ function newFloatingLayer () { const MODULE_NAME = 'Floating Layer' const ERROR_LOG = true const logger = newWebDebugLog() logger.fileName = MODULE_NAME let thisObject = { addFloatingObject: addFloatingObject, removeFloatingObject: removeFloatingObject, getFloatingObject: getFloatingObject, physics: physics, changeTargetRepulsion: changeTargetRepulsion, draw: draw, getContainer: getContainer, initialize: initialize, finalize: finalize } /* When objects are added to this layer they are added to the invisibleFloatingObjects array and they stay there because they are invisible. Visibility of objects is not decided at this layer, but outside. The ones who decides which object is visible or not are plotters, because they know the position of objects and the coordinate system which these positions belong to. To save resources, the physics engine only apply its calculations to visible floating objects. */ let invisibleFloatingObjects = [] /* Floating objects are accesible to plotters because they carry a "payload" object that was creted by them. This object includes the position and visible property. When visible turns to true, this layer module will move the reference of the floating object from the invisible array to the visibleFloatingObjects array. If the plotter decides that the object is to far from the what the user is seeing, it can eventually turn the visible property off again, forcing the layer to remove the object from the visibleFloatingObjects and place it again at the invisible array. */ let visibleFloatingObjects = [] let maxTargetRepulsionForce = 0.001 let currentHandle = 0 return thisObject function finalize () { invisibleFloatingObjects = [] visibleFloatingObjects = [] } function initialize () { /* We dont need to initialize anything right now. */ } function getContainer (point) { let container for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] container = floatingObject.getContainer(point) if (container !== undefined) { return container } } for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] container = floatingObject.getContainer(point) if (container !== undefined) { return container } } return container } function addFloatingObject (pFloatingObject) { currentHandle++ pFloatingObject.handle = currentHandle // Math.floor((Math.random() * 10000000) + 1); visibleFloatingObjects.push(pFloatingObject) } function removeFloatingObject (pFloatingObjectHandle) { for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] if (floatingObject.handle === pFloatingObjectHandle) { invisibleFloatingObjects.splice(i, 1) // Delete item from array. return } } for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] if (floatingObject.handle === pFloatingObjectHandle) { visibleFloatingObjects.splice(i, 1) // Delete item from array. return } } } function getFloatingObject (pFloatingObjectHandle, pFloatingObjectIndex) { /* There are two ways to look for a floating object: by handle or by index. The search by handle is done on the visible and invisible array. The search by index is done only on the visible ones. The search by index is usually needed when mouse events occurs. In those cases only objects visible to the end usser matters. */ try { if (pFloatingObjectHandle !== undefined) { for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] if (floatingObject.handle === pFloatingObjectHandle) { return floatingObject } } for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] if (floatingObject.handle === pFloatingObjectHandle) { return floatingObject } } } if (pFloatingObjectIndex !== undefined) { for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] if (i === pFloatingObjectIndex) { return floatingObject } } } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] getFloatingObject -> err= ' + err.stack) } } } function draw () { drawInvisibleObjects() drawVisibleObjects() } function drawInvisibleObjects () { for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] floatingObject.drawBackground() } for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] floatingObject.drawMiddleground() } for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[invisibleFloatingObjects.length - i - 1] floatingObject.drawForeground() } } function drawVisibleObjects () { for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] floatingObject.drawBackground() } for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] floatingObject.drawMiddleground() } for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[visibleFloatingObjects.length - i - 1] floatingObject.drawForeground() } /* Invisible objects on focus (freezed) should still have some priority */ for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] floatingObject.drawOnFocus() } for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] floatingObject.drawOnFocus() } } function makeVisible () { for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] if (floatingObject.isFrozen === false && floatingObject.isParentCollapsed === false) { visibleFloatingObjects.push(floatingObject) invisibleFloatingObjects.splice(i, 1) // Delete item from array. makeVisible() return // Only one at the time. } } } function makeInvisible () { for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] if (floatingObject.isFrozen === true || floatingObject.isParentCollapsed === true) { invisibleFloatingObjects.push(floatingObject) visibleFloatingObjects.splice(i, 1) // Delete item from array. makeInvisible() return // Only one at the time. } } } /******************************************/ /* */ /* Physics Engine Follows */ /* */ /******************************************/ function physics () { /* The Physics engine is hooked at the animation loop, so it executes very very often. According to this Physics engine, each floating object has a position and a speed. In fact, each floating object has two positions at the same time: 1. The current position: is the position of the ball, floating around. 2. The anchor position: is the position of where the ball is achored on the plot. This one is inside the payload object controlled by the plotter. The anchor position influences the current position, since the Physics engine will apply a gravity force to the ball's position in order to make it gravitate towards the anchor position. At the same time there will be a repulsion force that will prevent the ball reaching the anchor position, so that the anchor point keeps visible to the user. */ try { makeVisible() makeInvisible() applyPhysics() function applyPhysics () { /* This function makes all the calculations to apply phisycs on all visible floatingObjects in this layer. */ try { for (let i = 0; i < invisibleFloatingObjects.length; i++) { let floatingObject = invisibleFloatingObjects[i] floatingObject.physics() } for (let i = 0; i < visibleFloatingObjects.length; i++) { let floatingObject = visibleFloatingObjects[i] floatingObject.physics() /* From here on, only if they are not too far. */ if (canvas.floatingSpace.isItFar(floatingObject.payload)) { continue } if (floatingObject.positionLocked === false) { floatingObject.container.frame.position.x = floatingObject.container.frame.position.x + floatingObject.currentSpeed.x floatingObject.container.frame.position.y = floatingObject.container.frame.position.y + floatingObject.currentSpeed.y } if (floatingObject.friction < floatingObject.targetFriction) { floatingObject.friction = floatingObject.friction + 0.00001 } else { floatingObject.friction = floatingObject.friction - 0.00001 } floatingObject.currentSpeed.x = floatingObject.currentSpeed.x * floatingObject.friction // Desaceleration factor. floatingObject.currentSpeed.y = floatingObject.currentSpeed.y * floatingObject.friction // Desaceleration factor. let payload = { position: undefined, visible: false } switch (floatingObject.type) { case 'UI Object': { payload.targetPosition = floatingObject.payload.targetPosition payload.visible = floatingObject.payload.visible break } default: { break } } if (floatingObject.container.frame.position.x < payload.targetPosition.x) { floatingObject.currentSpeed.x = floatingObject.currentSpeed.x + 0.005 } else { floatingObject.currentSpeed.x = floatingObject.currentSpeed.x - 0.005 } if (floatingObject.container.frame.position.y < payload.targetPosition.y) { floatingObject.currentSpeed.y = floatingObject.currentSpeed.y + 0.005 } else { floatingObject.currentSpeed.y = floatingObject.currentSpeed.y - 0.005 } const MAX_SPEED = 10 if (floatingObject.currentSpeed.x > MAX_SPEED) { floatingObject.currentSpeed.x = MAX_SPEED } if (floatingObject.currentSpeed.y > MAX_SPEED) { floatingObject.currentSpeed.y = MAX_SPEED } if (floatingObject.currentSpeed.x < -MAX_SPEED) { floatingObject.currentSpeed.x = -MAX_SPEED } if (floatingObject.currentSpeed.y < -MAX_SPEED) { floatingObject.currentSpeed.y = -MAX_SPEED } // We let the Floating Object animate the physics loops by itself. checkBoundaries(floatingObject) /* Collision Control */ for (let k = i + 1; k < visibleFloatingObjects.length; k++) { if (colliding(visibleFloatingObjects[i], visibleFloatingObjects[k])) { resolveCollision(visibleFloatingObjects[k], visibleFloatingObjects[i]) } } /* Calculate repulsion force produced by all other floatingObjects */ repulsionForceBetweenFloatingObjects(i) targetRepulsionForce(i) gravityForce(floatingObject, payload) proximityBetweenFloatingObjects(i) } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] physics -> applyPhysics -> err= ' + err.stack) } } } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] physics -> err= ' + err.stack) } } } function checkBoundaries (floatingObject) { if (floatingObject.container.frame.position.x - floatingObject.container.frame.radius < 0) { floatingObject.container.frame.position.x = floatingObject.container.frame.radius floatingObject.currentSpeed.x = -floatingObject.currentSpeed.x } if (floatingObject.container.frame.position.x + floatingObject.container.frame.radius > canvas.floatingSpace.container.frame.width) { floatingObject.container.frame.position.x = canvas.floatingSpace.container.frame.width - floatingObject.container.frame.radius floatingObject.currentSpeed.x = -floatingObject.currentSpeed.x } if (floatingObject.container.frame.position.y - floatingObject.container.frame.radius < 0) { floatingObject.container.frame.position.y = floatingObject.container.frame.radius floatingObject.currentSpeed.y = -floatingObject.currentSpeed.y } if (floatingObject.container.frame.position.y + floatingObject.container.frame.radius > canvas.floatingSpace.container.frame.height) { floatingObject.container.frame.position.y = canvas.floatingSpace.container.frame.height - floatingObject.container.frame.radius floatingObject.currentSpeed.y = -floatingObject.currentSpeed.y } } function gravityForce (floatingObject, payload) { try { /* We simulate a kind of gravity towards the target point of each floatingObject. This force will make the floatingObject to keep pushing to reach that point. */ const coulomb = 0.00001 const minForce = 0.01 let d = Math.sqrt(Math.pow(payload.targetPosition.x - floatingObject.container.frame.position.x, 2) + Math.pow(payload.targetPosition.y - floatingObject.container.frame.position.y, 2)) // ... we calculate the distance ... let force = coulomb * d * d / floatingObject.currentMass // In this case the mass of the floatingObject affects the gravity force that it receives, that gives priority to target position to bigger floatingObjects. if (force < minForce) { // We need this attraction force to overcome the friction we imposed to the system. force = minForce } let pos1 = { x: floatingObject.container.frame.position.x, y: floatingObject.container.frame.position.y } let pos2 = { x: payload.targetPosition.x, y: payload.targetPosition.y } let posDiff = { // Next we need the vector resulting from the 2 positions. x: pos2.x - pos1.x, y: pos2.y - pos1.y } if (d !== 0) { let unitVector = { // To find the unit vector, we divide each component by the magnitude of the vector. x: posDiff.x / d, y: posDiff.y / d } let forceVector = { x: unitVector.x * force, y: unitVector.y * force } /* We add the force vector to the speed vector */ if (isNaN(forceVector.x) === false && isNaN(forceVector.y) === false) { floatingObject.currentSpeed.x = floatingObject.currentSpeed.x + forceVector.x floatingObject.currentSpeed.y = floatingObject.currentSpeed.y + forceVector.y } } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] gravityForce -> err= ' + err.stack) } } } function repulsionForceBetweenFloatingObjects (currentFloatingObject) { try { /* We generate a repulsion force between floatingObjects, that prevents them to be collisioning so often. */ const coulomb = 2 let floatingObject1 = visibleFloatingObjects[currentFloatingObject] for (let i = 0; i < visibleFloatingObjects.length; i++) { // The force to be applied is considering all other floatingObjects... if (i !== currentFloatingObject) { // ... except for the current one. let floatingObject2 = visibleFloatingObjects[i] // So, for each floatingObject... let d = Math.sqrt(Math.pow(floatingObject2.container.frame.position.x - floatingObject1.container.frame.position.x, 2) + Math.pow(floatingObject2.container.frame.position.y - floatingObject1.container.frame.position.y, 2)) // ... we calculate the distance ... let force = coulomb * floatingObject2.currentMass / (d * d) // ... and with it the repulsion force. /* We need to put a hard limit to this force, in order to to eject very little floatingObjects to the infinite and beyond. */ if (force > 1) { force = 1 } let pos1 = { x: floatingObject1.container.frame.position.x, y: floatingObject1.container.frame.position.y } let pos2 = { x: floatingObject2.container.frame.position.x, y: floatingObject2.container.frame.position.y } let posDiff = { // Next we need the vector resulting from the 2 positions. x: pos2.x - pos1.x, y: pos2.y - pos1.y } let unitVector = { // To find the unit vector, we divide each component by the magnitude of the vector. x: posDiff.x / d, y: posDiff.y / d } let forceVector = { x: unitVector.x * force, y: unitVector.y * force } /* We substract the force vector to the speed vector of the current floatingObject */ if (isNaN(forceVector.x) === false && isNaN(forceVector.y) === false) { floatingObject1.currentSpeed.x = floatingObject1.currentSpeed.x - forceVector.x floatingObject1.currentSpeed.y = floatingObject1.currentSpeed.y - forceVector.y } } } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] repulsionForceBetweenFloatingObjects -> err= ' + err.stack) } } } function proximityBetweenFloatingObjects (currentFloatingObject) { try { let floatingObject1 = visibleFloatingObjects[currentFloatingObject] floatingObject1.nearbyFloatingObjects = [] for (let i = 0; i < visibleFloatingObjects.length; i++) { if (i !== currentFloatingObject) { let floatingObject2 = visibleFloatingObjects[i] // So, for each floatingObject... let d = Math.sqrt(Math.pow(floatingObject2.container.frame.position.x - floatingObject1.container.frame.position.x, 2) + Math.pow(floatingObject2.container.frame.position.y - floatingObject1.container.frame.position.y, 2)) // ... we calculate the distance ... if (floatingObject1.nearbyFloatingObjects.length === 0) { floatingObject1.nearbyFloatingObjects.push([d, floatingObject2]) } else { let recordAdded = false for (let j = 0; j < floatingObject1.nearbyFloatingObjects.length; j++) { let recordedDistance = floatingObject1.nearbyFloatingObjects[j][0] if (d < recordedDistance) { floatingObject1.nearbyFloatingObjects.splice(j, 0, [d, floatingObject2]) recordAdded = true break } } if (recordAdded === false) { floatingObject1.nearbyFloatingObjects.push([d, floatingObject2]) } } floatingObject1.nearbyFloatingObjects.splice(10, floatingObject1.nearbyFloatingObjects.length) } } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] proximityBetweenFloatingObjects -> err= ' + err.stack) } } } function targetRepulsionForce (currentFloatingObject) { try { /* We generate a repulsion force between floatingObjects, that prevents them to be collisioning so often. */ const coulomb = 2 let floatingObject1 = visibleFloatingObjects[currentFloatingObject] for (let i = 0; i < visibleFloatingObjects.length; i++) { // The force to be applied is considering all other floatingObjects... let floatingObject2 = visibleFloatingObjects[i] // So, for each floatingObject... let payload = { position: undefined } switch (floatingObject2.type) { case 'UI Object': { payload.targetPosition = floatingObject2.payload.targetPosition break } default: { break } } let d = Math.sqrt(Math.pow(payload.targetPosition.x - floatingObject1.container.frame.position.x, 2) + Math.pow(payload.targetPosition.y - floatingObject1.container.frame.position.y, 2)) // ... we calculate the distance ... let force = coulomb * floatingObject2.currentMass / (d * d) // ... and with it the repulsion force. /* We need to put a hard limit to this force, in order to to eject very little floatingObjects to the infinite and beyond. */ if (force > maxTargetRepulsionForce) { force = maxTargetRepulsionForce } let pos1 = { x: floatingObject1.container.frame.position.x, y: floatingObject1.container.frame.position.y } let pos2 = { x: payload.targetPosition.x, y: payload.targetPosition.y } let posDiff = { // Next we need the vector resulting from the 2 positions. x: pos2.x - pos1.x, y: pos2.y - pos1.y } if (d !== 0) { let unitVector = { // To find the unit vector, we divide each component by the magnitude of the vector. x: posDiff.x / d, y: posDiff.y / d } let forceVector = { x: unitVector.x * force, y: unitVector.y * force } /* We substract the force vector to the speed vector of the current floatingObject */ if (isNaN(forceVector.x) === false && isNaN(forceVector.y) === false) { floatingObject1.currentSpeed.x = floatingObject1.currentSpeed.x - forceVector.x floatingObject1.currentSpeed.y = floatingObject1.currentSpeed.y - forceVector.y } } } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] targetRepulsionForce -> err= ' + err.stack) } } } function changeTargetRepulsion (pDelta) { try { if (pDelta > 0) { pDelta = 1 } else { pDelta = -1 } maxTargetRepulsionForce = maxTargetRepulsionForce + pDelta / 1000 if (maxTargetRepulsionForce < 0.0000000001) { maxTargetRepulsionForce = 0.0000000001 } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] changeTargetRepulsion -> err= ' + err.stack) } } } function colliding (floatingObject1, floatingObject2) { try { /* This function detects weather 2 floatingObjects collide with each other. */ let r1 = floatingObject1.container.frame.radius let r2 = floatingObject2.container.frame.radius let distance = Math.sqrt(Math.pow(floatingObject2.container.frame.position.x - floatingObject1.container.frame.position.x, 2) + Math.pow(floatingObject2.container.frame.position.y - floatingObject1.container.frame.position.y, 2)) if (distance > (r1 + r2)) { // No solutions, the circles are too far apart. return false } else if (distance <= r1 + r2) { // One circle contains the other. return true } else if ((distance === 0) && (r1 === r2)) { // the circles coincide. return true } else return true } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] colliding -> err= ' + err.stack) } } } function distance (x1, y1, x2, y2) { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)) } function resolveCollision (floatingObject1, floatingObject2) { try { /* This function changes speed and position of floatingObjects that are in collision */ let collisionision_angle = Math.atan2((floatingObject2.container.frame.position.y - floatingObject1.container.frame.position.y), (floatingObject2.container.frame.position.x - floatingObject1.container.frame.position.x)) let speed1 = Math.sqrt(floatingObject1.currentSpeed.x * floatingObject1.currentSpeed.x + floatingObject1.currentSpeed.y * floatingObject1.currentSpeed.y) // Magnitude of Speed Vector for floatingObject 1 let speed2 = Math.sqrt(floatingObject2.currentSpeed.x * floatingObject2.currentSpeed.x + floatingObject2.currentSpeed.y * floatingObject2.currentSpeed.y) // Magnitude of Speed Vector for floatingObject 2 let direction_1 = Math.atan2(floatingObject1.currentSpeed.y, floatingObject1.currentSpeed.x) let direction_2 = Math.atan2(floatingObject2.currentSpeed.y, floatingObject2.currentSpeed.x) let new_xspeed_1 = speed1 * Math.cos(direction_1 - collisionision_angle) let new_yspeed_1 = speed1 * Math.sin(direction_1 - collisionision_angle) let new_xspeed_2 = speed2 * Math.cos(direction_2 - collisionision_angle) let new_yspeed_2 = speed2 * Math.sin(direction_2 - collisionision_angle) let final_xspeed_1 = ((floatingObject1.currentMass - floatingObject2.currentMass) * new_xspeed_1 + (floatingObject2.currentMass + floatingObject2.currentMass) * new_xspeed_2) / (floatingObject1.currentMass + floatingObject2.currentMass) let final_xspeed_2 = ((floatingObject1.currentMass + floatingObject1.currentMass) * new_xspeed_1 + (floatingObject2.currentMass - floatingObject1.currentMass) * new_xspeed_2) / (floatingObject1.currentMass + floatingObject2.currentMass) let final_yspeed_1 = new_yspeed_1 let final_yspeed_2 = new_yspeed_2 let cosAngle = Math.cos(collisionision_angle) let sinAngle = Math.sin(collisionision_angle) floatingObject1.currentSpeed.x = cosAngle * final_xspeed_1 - sinAngle * final_yspeed_1 floatingObject1.currentSpeed.y = sinAngle * final_xspeed_1 + cosAngle * final_yspeed_1 floatingObject2.currentSpeed.x = cosAngle * final_xspeed_2 - sinAngle * final_yspeed_2 floatingObject2.currentSpeed.y = sinAngle * final_xspeed_2 + cosAngle * final_yspeed_2 let pos1 = { x: floatingObject1.container.frame.position.x, y: floatingObject1.container.frame.position.y } let pos2 = { x: floatingObject2.container.frame.position.x, y: floatingObject2.container.frame.position.y } // get the mtd let posDiff = { x: pos1.x - pos2.x, y: pos1.y - pos2.y } let d = Math.sqrt(Math.pow(floatingObject2.container.frame.position.x - floatingObject1.container.frame.position.x, 2) + Math.pow(floatingObject2.container.frame.position.y - floatingObject1.container.frame.position.y, 2)) // minimum translation distance to push floatingObjects apart after intersecting let scalar = (((floatingObject1.container.frame.radius + floatingObject2.container.frame.radius) - d) / d) let minTD = { x: posDiff.x * scalar, y: posDiff.y * scalar } // resolve intersection -- // computing inverse mass quantities let im1 = 1 / floatingObject1.currentMass let im2 = 1 / floatingObject2.currentMass // push-pull them apart based off their mass pos1.x = pos1.x + minTD.x * (im1 / (im1 + im2)) pos1.y = pos1.y + minTD.y * (im1 / (im1 + im2)) pos2.x = pos2.x - minTD.x * (im2 / (im1 + im2)) pos2.y = pos2.y - minTD.y * (im2 / (im1 + im2)) if (floatingObject1.positionLocked === false && isNaN(pos1.x) === false && isNaN(pos1.y) === false) { floatingObject1.container.frame.position = pos1 } if (floatingObject2.positionLocked === false && isNaN(pos2.x) === false && isNaN(pos2.y) === false) { floatingObject2.container.frame.position = pos2 } } catch (err) { if (ERROR_LOG === true) { logger.write('[ERROR] resolveCollision -> err= ' + err.stack) } } } }