Implementing bullet collisions in Matter.js for a shooting game - javascript

I am trying to make a shooting game in matter.js but can't find a way to shoot bullets from the player's exact location and how to count the collision between player and bullet but not with the walls.
I want to fire a bullet from player1 and then on pressing D again it should fire another bullet from the player1's last position.
My Codepen of this game
let p1= Matter.Bodies.polygon(200, 200, 3, 40, {
chamfer: {
radius: [15,10,15]
},
isStatic: false,
inertia: Infinity,
friction: 0.9,
render: {
fillStyle: '#F9ED69'
},
mass:1
});
let p2 = Matter.Bodies.polygon(1100, 200, 3, 40, {
chamfer: {
radius: [15,10,15]
},
isStatic: false,
inertia: Infinity,
friction: 0.9,
render: {
fillStyle: '#11999E'
},
mass:1
});
let bullet1 = Matter.Bodies.polygon(400, 300, 3, 7, {
chamfer: {
radius: [4,2,4]
},
isStatic: false,
inertia: Infinity,
friction: 0.9,
render: {
fillStyle: '#F9ED69'
},
mass:0
});
const keyHandlers = {
KeyS: () => {
Matter.Body.applyForce(p1, {
x: p1.position.x,
y: p1.position.y
}, {x: 0.0, y: 0.001})
},
KeyW: () => {
Matter.Body.applyForce(p1, {
x: p1.position.x,
y: p1.position.y
}, {x: 0.0, y: -0.002})
},
KeyD:()=>{
Matter.Body.applyForce(bullet1, {
x: p1.position.x,
y: p1.position.y
}, {x: 0.001, y: 0.0})
},
};
const keysDown = new Set();
document.addEventListener("keydown", event => {
keysDown.add(event.code);
});
document.addEventListener("keyup", event => {
keysDown.delete(event.code);
});
Matter.Events.on(engine, "beforeUpdate", event => {
[...keysDown].forEach(k => {
keyHandlers[k]?.();
});
});
// on collision of a bullet with wall and other bodies remove the bullet from the world after some delay and add the score
let score1 = 0;
let score2 = 0;
let health
Matter.Events.on(engine, "collisionStart", event => {
for (let i = 0; i < event.pairs.length; i++) {
const pair = event.pairs[i];
if (pair.bodyA === bullet1 || pair.bodyB === bullet1) {
Matter.World.remove(engine.world, bullet1);
alert('1');
}
if (pair.bodyA === bullet2 || pair.bodyB === bullet2) {
Matter.World.remove(engine.world, bullet2);
alert('2');
}
}
score1++;
alert(`SCore1 is ${score1}`); // these alerts are just to confirm the collision
});

You're on the right track, but if you hardcode bullet1 and bullet2 you're stuck with just those two bullets. Even with a fixed number of bullets and re-using the bodies (good for performance but maybe premature optimization), I'd probably use an array to store these bullets, which is almost always the correct move after you catch yourself doing thing1, thing2...
Here's a proof of concept. I'm creating and destroying bullets here to keep the coding easier, but it'd be more performant to keep a pool of objects and re-use/re-position them.
I'm also using sets to keep track of the types of the bodies, but you might want to use labels. Most of the code here could go in many different directions, specific to your use case.
const engine = Matter.Engine.create();
engine.gravity.y = 0; // enable top-down
const map = {width: 300, height: 300};
const render = Matter.Render.create({
element: document.body,
engine,
options: {...map, wireframes: false},
});
const player = {
score: 0,
body: Matter.Bodies.polygon(
map.width / 2, map.height / 2, 3, 15, {
frictionAir: 0.06,
density: 0.9,
render: {fillStyle: "red"},
},
),
lastShot: Date.now(),
cooldown: 150,
fireForce: 0.1,
rotationAngVel: 0.03,
rotationAmt: 0.03,
rotateLeft() {
Matter.Body.rotate(this.body, -this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, -this.rotationAngVel
);
},
rotateRight() {
Matter.Body.rotate(this.body, this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, this.rotationAngVel
);
},
fire() {
if (Date.now() - this.lastShot < this.cooldown) {
return;
}
// move the bullet away from the player a bit
const {x: bx, y: by} = this.body.position;
const x = bx + (Math.cos(this.body.angle) * 10);
const y = by + (Math.sin(this.body.angle) * 10);
const bullet = Matter.Bodies.circle(
x, y, 4, {
frictionAir: 0.006,
density: 0.1,
render: {fillStyle: "yellow"},
},
);
bullets.add(bullet);
Matter.Composite.add(engine.world, bullet);
Matter.Body.applyForce(
bullet, this.body.position, {
x: Math.cos(this.body.angle) * this.fireForce,
y: Math.sin(this.body.angle) * this.fireForce,
},
);
this.lastShot = Date.now();
},
};
const bullets = new Set();
const makeEnemy = () => Matter.Bodies.polygon(
(Math.random() * (map.width - 40)) + 20,
(Math.random() * (map.height - 40)) + 20,
5, 6, {
render: {
fillStyle: "transparent",
strokeStyle: "white",
lineWidth: 1,
},
},
);
const enemies = new Set([...Array(100)].map(makeEnemy));
const walls = new Set([
Matter.Bodies.rectangle(
0, map.height / 2, 20, map.height, {isStatic: true}
),
Matter.Bodies.rectangle(
map.width / 2, 0, map.width, 20, {isStatic: true}
),
Matter.Bodies.rectangle(
map.width, map.height / 2, 20, map.height, {isStatic: true}
),
Matter.Bodies.rectangle(
map.width / 2, map.height, map.width, 20, {isStatic: true}
),
]);
Matter.Composite.add(engine.world, [
player.body, ...walls, ...enemies
]);
const keyHandlers = {
ArrowLeft: () => player.rotateLeft(),
ArrowRight: () => player.rotateRight(),
Space: () => player.fire(),
};
const validKeys = new Set(Object.keys(keyHandlers));
const keysDown = new Set();
document.addEventListener("keydown", e => {
if (validKeys.has(e.code)) {
e.preventDefault();
keysDown.add(e.code);
}
});
document.addEventListener("keyup", e => {
if (validKeys.has(e.code)) {
e.preventDefault();
keysDown.delete(e.code);
}
});
Matter.Events.on(engine, "beforeUpdate", event => {
[...keysDown].forEach(k => {
keyHandlers[k]?.();
});
if (enemies.size < 100 && Math.random() > 0.95) {
const enemy = makeEnemy();
enemies.add(enemy);
Matter.Composite.add(engine.world, enemy);
}
});
Matter.Events.on(engine, "collisionStart", event => {
for (const {bodyA, bodyB} of event.pairs) {
const [a, b] = [bodyA, bodyB].sort((a, b) =>
bullets.has(a) ? -1 : 1
);
if (bullets.has(a) && walls.has(b)) {
Matter.Composite.remove(engine.world, a);
bullets.delete(a);
}
else if (bullets.has(a) && enemies.has(b)) {
Matter.Composite.remove(engine.world, a);
Matter.Composite.remove(engine.world, b);
bullets.delete(a);
enemies.delete(b);
document.querySelector("span").textContent = ++player.score;
}
}
});
Matter.Render.run(render);
Matter.Runner.run(Matter.Runner.create(), engine);
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<div>press left/right arrow keys to rotate and space to shoot</div>
<div>score: <span>0</span></div>

Related

PhaserRexUI Plugin Not displaying

So I've been trying to use the RexRainbow Phaser UI plugin, and All the Ui i make is invisible for some reason, But when I draw boundaries, it draws them, leaving me with a bunch of red boxes. Why are they all invisible?
Code Here (Github Gist)
//UI
var tabs = this.rexUI.add
.tabs({
x: 400,
y: 1600,
panel: this.rexUI.add.gridTable({
background: this.rexUI.add.roundRectangle(
0,
0,
20,
10,
10,
0x4e342e
),
table: {
width: 250,
height: 400,
cellWidth: 120,
cellHeight: 60,
columns: 1,
mask: {
padding: 2,
},
},
slider: {
//scroll bar
track: this.rexUI.add.roundRectangle(
0,
0,
20,
10,
10,
this.COLOR_DARK
),
thumb: this.rexUI.add.roundRectangle(
0,
0,
5,
40,
10,
this.COLOR_LIGHT
),
}
.layout()
.drawBounds(this.add.graphics(), 0xff0000); //debug for ui
https://codepen.io/vatsadev/pen/dyqGNBG -> full working example
It is hard too say, but I just can assume, that the reason is, that the color's used (that are not visible) are probally undefined and that's why transparent/invisible.
Without knowing the whole code, it is best to check, the variables/properties used for colors (like: this.COLOR_LIGHT, this.COLOR_DARK, ...)
Especially line 62, since here the this context is local to the tabs- object, and is not the scene object.
Tipp: for debugging purposes, I would hardcode all colors, just to see if the setup works, as intended. If so start replacing the hardcoded values with variables, like this you will find the culprit fast.
document.body.style = 'margin:0;';
const COLOR_PRIMARY = 0x4e342e;
const COLOR_LIGHT = 0x7b5e57;
const COLOR_DARK = 0x260e04;
var config = {
type: Phaser.AUTO,
width: 536,
height: 283,
scene: {
preload,
create
}
};
var isLeaking = false;
function preload (){
this.load.image('tiles', 'https://labs.phaser.io/assets/tilemaps/tiles/catastrophi_tiles_16.png');
this.load.tilemapCSV('map', 'https://labs.phaser.io/assets/tilemaps/csv/catastrophi_level2.csv');
this.load.scenePlugin({
key: "rexuiplugin",
url: "https://raw.githubusercontent.com/rexrainbow/phaser3-rex-notes/master/dist/rexuiplugin.min.js",
sceneKey: "rexUI",
});
}
function create () {
let map = this.make.tilemap({ key: 'map', tileWidth: 16, tileHeight: 16 });
let tileset = map.addTilesetImage('tiles');
let fgLayer = map.createLayer(0, tileset, 0, 0);
createUi(this);
updateMap(map);
}
function updateMap (map) {
let originPoint1 = map.getTileAtWorldXY(200, 100);
console.info(map.layers.sort((a,b) => b.depth - a.depth))
map.forEachTile(function (tile) {
var dist = Phaser.Math.Distance.Chebyshev(
originPoint1.x,
originPoint1.y,
tile.x,
tile.y
);
tile.setAlpha(1 - 0.09 * dist);
});
}
function createDataBase () {
var inventory = ['grass', 2, 'dirt', 3, 'wood', 2, 'leaves', 2, ]
// Create the database
var db = new loki();
// Create a collection
var items = db.addCollection("items");
// Insert documents
for (var i = 0; i < inventory.length; i+=2) {
items.insert({
blockType: inventory[i],
quantity: inventory[i+1],
color: Phaser.Math.Between(0, 0xffffff),
});
}
return items;
};
function createUi(scene){
var db = createDataBase();
var tabs = scene.rexUI.add
.tabs({
x: 250,
y: 250,
panel: scene.rexUI.add.gridTable({
background: scene.rexUI.add.roundRectangle(
0,
0,
20,
10,
10,
COLOR_PRIMARY
),
table: {
width: 250,
height: 400,
cellWidth: 120,
cellHeight: 60,
columns: 1,
mask: {
padding: 2,
},
},
slider: { //scroll bar
track: scene.rexUI.add.roundRectangle(0, 0, 20, 10, 10, COLOR_DARK),
thumb: scene.rexUI.add.roundRectangle(0, 0, 5, 40, 10, COLOR_LIGHT),
},
createCellContainerCallback: function (cell) { // each inventory cell
var scene = cell.scene;
var width = 250;
var height = cell.height;
var item = cell.item;
var index = cell.index;
return scene.rexUI.add.label({
width: width,
height: height,
background: scene.rexUI.add
.roundRectangle(0, 0, 20, 20, 0)
.setStrokeStyle(2, COLOR_DARK),
icon: scene.rexUI.add.roundRectangle( // inventory item texture goes here
0,
0,
20,
20,
10,
item.color
),
text: scene.add.text(0, 0, `${item.blockType}: ${item.quantity}`),
space: {
icon: 10,
left: 15,
},
});
},
}),
leftButtons: [
createButton(scene, 2, "Inv."),
],
space: {
leftButtonsOffset: 20,
leftButton: 1,
},
})
.layout()
.drawBounds(scene.add.graphics(), 0xff0000);
tabs.on(
"button.click",
function () {
// Load items into grid table
var items = db
.chain()
.data();
this.getElement("panel").setItems(items).scrollToTop();
},
tabs
);
tabs.emitButtonClick("left", 0);
}
function createButton (scene, direction, text) {
var radius;
switch (direction) {
case 0: // Right
radius = {
tr: 20,
br: 20,
};
break;
case 2: // Left
radius = {
tl: 20,
bl: 20,
};
break;
}
return scene.rexUI.add.label({
width: 50,
height: 40,
background: scene.rexUI.add.roundRectangle(
0,
0,
50,
50,
radius,
COLOR_DARK
),
text: scene.add.text(0, 0, text, {
fontSize: "18pt",
}),
space: {
left: 10,
},
});
};
new Phaser.Game(config);
<script src="//cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lokijs/1.5.5/lokijs.min.js"></script>

How do I use Ammo.btCompoundShape (JavaScript port of Bullet Physics)?

I'm using Ammo.js, a direct JavaScript port of C++ Bullet Physics. The unfortunate result being that the documentation is C++, not great reading if your languages are Python and JavaScript.
I have the documentation for Ammo.btCompoundShape here but can't make sense of it.
I have a working code here where the Bone instance just falls through the floor, as you'll see. Don't worry about the naming of "Bone", at this stage in development it's just meant to test a compound shape of two blocks.
class RenderEngine {
constructor(gameEngine) {
this.gameEngine = gameEngine
this.gameEngine.clock = new THREE.Clock();
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xbfd1e5);
this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.2, 5000);
this.camera.position.set(0, 30, 70);
this.camera.lookAt(new THREE.Vector3(0, 0, 0));
const hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.1);
hemiLight.color.setHSL(0.6, 0.6, 0.6);
hemiLight.groundColor.setHSL(0.1, 1, 0.4);
hemiLight.position.set(0, 50, 0);
this.scene.add(hemiLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.color.setHSL(0.1, 1, 0.95);
dirLight.position.set(-1, 1.75, 1);
dirLight.position.multiplyScalar(100);
this.scene.add(dirLight);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
const d = 50;
dirLight.shadow.camera.left = -d;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = -d;
dirLight.shadow.camera.far = 13500;
this.renderer = new THREE.WebGLRenderer({
antialias: true
});
this.renderer.setClearColor(0xbfd1e5);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
this.renderer.shadowMap.enabled = true;
}
renderFrame() {
this.renderer.render(this.scene, this.camera)
}
}
class PhysicsEngine {
constructor(gameEngine, physicsEngine) {
this.gameEngine = gameEngine
let collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(),
dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration),
overlappingPairCache = new Ammo.btDbvtBroadphase(),
solver = new Ammo.btSequentialImpulseConstraintSolver();
this.tmpTrans = new Ammo.btTransform();
this.physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
this.physicsWorld.setGravity(new Ammo.btVector3(0, -10, 0));
}
updateFrame() {
this.physicsWorld.stepSimulation(this.gameEngine.clock.getDelta(), 10);
this.gameEngine.objects.forEach(object => {
const ms = object.ammo.getMotionState()
if (ms) {
ms.getWorldTransform(this.tmpTrans)
const p = this.tmpTrans.getOrigin()
const q = this.tmpTrans.getRotation()
object.mesh.position.set(p.x(), p.y(), p.z())
object.mesh.quaternion.set(q.x(), q.y(), q.z(), q.w())
}
})
}
}
class GameEngine {
constructor(renderEngine, physicsEngine) {
this.objects = []
this.renderEngine = new RenderEngine(this, renderEngine)
this.physicsEngine = new PhysicsEngine(this, physicsEngine)
}
run() {
this.physicsEngine.updateFrame()
this.renderEngine.renderFrame()
requestAnimationFrame(() => {
this.run()
});
}
add(object) {
this.objects.push(object)
return this.objects.length - 1
}
remove(objectIndex) {
this.objects[objectIndex] = false
}
}
class Box {
constructor(gameEngine, properties) {
this.gameEngine = gameEngine
this._initPhysics_(properties)
this._initRendering_(properties)
this.id = gameEngine.add(this)
}
_initPhysics_(properties) {
const pos = properties.pos
const quat = properties.quat
const scale = properties.scale
const mass = properties.mass
const group = properties.group
const interactionGroup = properties.interactionGroup
const physicsWorld = this.gameEngine.physicsEngine.physicsWorld
const transform = new Ammo.btTransform()
transform.setIdentity()
transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z))
transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w))
const motionState = new Ammo.btDefaultMotionState(transform)
const colShape = new Ammo.btBoxShape(new Ammo.btVector3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5))
colShape.setMargin(0.05)
const localInertia = new Ammo.btVector3(0, 0, 0)
colShape.calculateLocalInertia(mass, localInertia)
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, colShape, localInertia)
const body = new Ammo.btRigidBody(rbInfo)
physicsWorld.addRigidBody(body, group, interactionGroup)
this.ammo = body
}
_initRendering_(properties) {
const pos = properties.pos
const scale = properties.scale
const color = properties.color
this.mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshPhongMaterial({
color
}))
this.mesh.position.set(pos.x, pos.y, pos.z)
this.mesh.scale.set(scale.x, scale.y, scale.z)
this.mesh.castShadow = true
this.mesh.receiveShadow = true
this.gameEngine.renderEngine.scene.add(this.mesh)
}
}
class Bone {
constructor(gameEngine, properties) {
this.gameEngine = gameEngine
this._initPhysics_(properties)
this._initRendering_(properties)
this.id = gameEngine.add(this)
}
_initPhysics_(properties) {
const pos = properties.pos
const quat = properties.quat
const scale = properties.scale
const mass = properties.mass
const group = properties.group
const interactionGroup = properties.interactionGroup
const physicsWorld = this.gameEngine.physicsEngine.physicsWorld
const compoundShape = new Ammo.btCompoundShape()
this._addSection_(compoundShape, {
pos,
quat,
scale,
offset: {
x: 0,
y: 0,
z: 0
},
rotation: {
x: 0,
y: 0,
z: 0,
w: 0
}
})
this._addSection_(compoundShape, {
pos,
quat,
scale,
offset: {
x: 0,
y: 0,
z: 0
},
rotation: {
x: 0,
y: 0,
z: 0,
w: 0
}
})
const transform = new Ammo.btTransform()
transform.setIdentity()
transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z))
transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w))
const motionState = new Ammo.btDefaultMotionState(transform)
compoundShape.setMargin(0.05)
const localInertia = new Ammo.btVector3(0, 0, 0)
compoundShape.calculateLocalInertia(mass, localInertia)
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, compoundShape, localInertia)
const body = new Ammo.btRigidBody(rbInfo)
physicsWorld.addRigidBody(body, group, interactionGroup)
this.ammo = body
}
_initRendering_(properties) {
const pos = properties.pos
const scale = properties.scale
const color = properties.color
this.mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshPhongMaterial({
color
}))
this.mesh.position.set(pos.x, pos.y, pos.z)
this.mesh.scale.set(scale.x, scale.y, scale.z)
this.mesh.castShadow = true
this.mesh.receiveShadow = true
this.gameEngine.renderEngine.scene.add(this.mesh)
}
_addSection_(compoundShape, properties) {
const pos = properties.pos
const quat = properties.quat
const scale = properties.scale
const offset = properties.offset
const rotation = properties.rotation
const transform = new Ammo.btTransform()
transform.setIdentity()
transform.setOrigin(new Ammo.btVector3(pos.x + offset.x, pos.y + offset.y, pos.z + offset.z))
transform.setRotation(new Ammo.btQuaternion(quat.x + rotation.x, quat.y + rotation.y, quat.z + rotation.z, quat.w + rotation.w))
const motionState = new Ammo.btDefaultMotionState(transform)
const colShape = new Ammo.btBoxShape(new Ammo.btVector3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5))
compoundShape.addChildShape(transform, colShape)
}
}
Ammo().then((Ammo) => {
const gameEngine = new GameEngine(THREE, Ammo)
const plane = new Box(gameEngine, {
pos: {
x: 0,
y: 0,
z: 0
},
quat: {
x: 0,
y: 0,
z: 0,
w: 1
},
scale: {
x: 50,
y: 2,
z: 50
},
mass: 0,
group: 1,
interactionGroup: 1,
color: 0xa0afa4
})
const box1 = new Box(gameEngine, {
pos: {
x: 0,
y: 5,
z: 0
},
quat: {
x: 0,
y: 0,
z: 0,
w: 1
},
scale: {
x: 2,
y: 2,
z: 2
},
mass: 1,
group: 1,
interactionGroup: 1,
color: 0xa0afa4
})
const box2 = new Box(gameEngine, {
pos: {
x: 0.75,
y: 8,
z: 0.75
},
quat: {
x: 0,
y: 0,
z: 0,
w: 1
},
scale: {
x: 2,
y: 2,
z: 2
},
mass: 1,
group: 1,
interactionGroup: 1,
color: 0xa0afa4
})
const bone1 = new Bone(gameEngine, {
pos: {
x: -0.75,
y: 10,
z: -0.75
},
quat: {
x: 0,
y: 0,
z: 0,
w: 1
},
scale: {
x: 2,
y: 2,
z: 2
},
mass: 1,
group: 1,
interactionGroup: 1,
color: 0xa0afa4
})
console.log("gameEngine", gameEngine)
gameEngine.run()
})
canvas, body, html {
margin: 0px;
padding: 0px;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r124/three.min.js"></script>
<script src="https://cdn.babylonjs.com/ammo.js"></script>
The two Box instances land on the floor (plane), bone1 falls through. I assume I did something wrong with the Ammo.btCompoundShape. There are no errors. What's the correct way?
The bone actually does not fall through completely, it stops in the middle of the plane.
Reason: You are transforming two times:
motionState is transformed
compoundShape is transformed again inside of _addSection_
This way the compoundShape does collide and does not fall through, but the visible colShape is offset (from the reference position of compoundShape) to be inside the plane.
You can see that if you try and change this line inside _addSection_:
transform.setOrigin(new Ammo.btVector3(0, 0, 2.0))
Solution:
Do not transform two times. E.g. transform only the motionState, but not the compoundShape.
E.g. remove these two lines:
_addSection_(compoundShape, properties) {
const transform = new Ammo.btTransform()
transform.setIdentity()
// -- disable second transform: --
// transform.setOrigin(new Ammo.btVector3( ... ))
// transform.setRotation(new Ammo.btQuaternion( ... ))
const colShape = new Ammo.btBoxShape(new Ammo.btVector3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5))
compoundShape.addChildShape(transform, colShape)
}
Another remark:
localInertia is also applied to two times:
compoundShape.calculateLocalInertia(mass, localInertia) and
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, compoundShape, localInertia).
This obviously works, but is probably not intentional. It doesn't fail because it is 0,0,0. If you indeed want two inertia, I also think you can not use the same localInertia object for both of them, but you should create a second object e.g. localInertiaCompoundShape, but I'm not sure.

Matter.js — How to get the dimension of an image to set Bodies sizes?

I am trying to programmatically set the width and heights of the chained bodies in matter.js. Unfortunately, I am only getting 0 as values and I am unsure why. My guess is that the images are not being loaded fast enough to provide those values. How can I load those dimensions before the images are loaded?
Pseudo-code
Several bodies from Array
Get the width and height of each image in the Array
Use this value to set the Bodies dimensions
Code
var playA = Composites.stack(
percentX(25) - assetSize / 2,
percentY(25),
1,
6,
5,
5,
function (x, y) {
iA++;
var imgWidth;
var imgHeight;
var img = new Image();
img.src = String(design[iA]);
var imgWidth = 0;
var imgHeight = 0;
img.onload = function a() {
imgWidth = img.naturalWidth;
imgHeight = img.naturalHeight;
console.log(String(design[iA]), imgWidth, imgHeight);
};
console.log(String(design[iA]), imgHeight, imgWidth); // I can't access the values here.
return Bodies.rectangle(x, y, imgWidth, imgHeight, {
// collisionFilter: { group: group },
friction: 1,
render: {
sprite: {
texture: design[iA],
xScale: (assetSize / 100) * 0.46,
yScale: (assetSize / 100) * 0.46
}
}
});
}
);
Composites.chain(playA, 0.3, 0, -0.5, 0, {
stiffness: 1,
length: 10,
render: { type: "line", visible: false }
});
If you know the dimensions and can populate an array beforehand, the solution is potentially straightforward since Matter.js loads images given a URL string, with the caveat that the engine doesn't wait for the loads before running.
Here's a minimal example of iterating over width/height pairs in an array and passing these properties into the rectangle calls which I'll use as a stepping stone to the example that matches your use case.
const engine = Matter.Engine.create();
const render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: 450,
height: 250,
wireframes: false, // required for images
}
});
Matter.Render.run(render);
const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);
const imgSizes = [[56, 48], [45, 50], [35, 50], [60, 63]];
const stack = Matter.Composites.stack(
// xx, yy, columns, rows, columnGap, rowGap, cb
150, 50, 4, 1, 0, 0,
(x, y, i) => {
const [w, h] = imgSizes[i];
return Matter.Bodies.rectangle(x, y, w, h, {
render: {
sprite: {
texture: `http://placekitten.com/${w}/${h}`
}
}
});
}
);
Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
stiffness: 0.75,
length: 10,
render: {type: "line", visible: true}
});
Matter.Composite.add(engine.world, [
stack,
Matter.Bodies.rectangle(225, 0, 450, 25, {
isStatic: true
}),
Matter.Bodies.rectangle(450, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(0, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(225, 250, 450, 25, {
isStatic: true
})
]);
const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {visible: true}
}
});
Matter.Composite.add(engine.world, mouseConstraint);
render.mouse = mouse;
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
Now, if you need to load images using onload and use their dimensions, you'll need to use promises or put all code dependent on these images into the sequence of onload callback(s) as described in the canonical How do I return the response from an asynchronous call?.
The failing pattern is:
const getSomethingAsync = cb => setTimeout(() => cb("something"), 0);
let data = null;
getSomethingAsync(result => {
data = result;
console.log("this runs last");
});
console.log(data); // guaranteed to be null, not "something"
// more logic that is supposed to depend on data
The fix is:
const getSomethingAsync = cb => setTimeout(() => cb("something"), 0);
getSomethingAsync(data => {
console.log(data);
// logic that depends on the data from `getSomethingAsync`
});
console.log("this will run first");
// logic that doesn't depend on data from `getSomethingAsync`
Since you're juggling multiple onloads, you can promisify the onloads to make them easier to work with. I have a couple examples of doing this here and here agnostic of matter.js.
Here's an example of using promises to load images applied to your general problem. Again, I'll use my own code so that it's runnable and reproducible, but the pattern should be easy to extrapolate to your project.
The idea is to first load the images using a series of promises which are resolved when onload handlers fire, then use Promise.all to chain a then which runs the MJS initializer callback only when all images are loaded. The widths and heights are then accessible to your matter.js code within the callback.
As a side benefit, this ensures images are loaded by the time MJS runs.
const initializeMJS = images => {
const engine = Matter.Engine.create();
const render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: 450,
height: 250,
wireframes: false, // required for images
}
});
Matter.Render.run(render);
const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);
const stack = Matter.Composites.stack(
// xx, yy, columns, rows, columnGap, rowGap, cb
150, 50, 4, 1, 0, 0,
(x, y, i) => {
const {width: w, height: h} = images[i];
return Matter.Bodies.rectangle(x, y, w, h, {
render: {
sprite: {
texture: images[i].src
}
}
});
}
);
Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
stiffness: 0.75,
length: 10,
render: {type: "line", visible: true}
});
Matter.Composite.add(engine.world, [
stack,
Matter.Bodies.rectangle(225, 0, 450, 25, {
isStatic: true
}),
Matter.Bodies.rectangle(450, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(0, 150, 25, 300, {
isStatic: true
}),
Matter.Bodies.rectangle(225, 250, 450, 25, {
isStatic: true
})
]);
const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {visible: true}
}
});
Matter.Composite.add(engine.world, mouseConstraint);
render.mouse = mouse;
};
const imageSizes = [[56, 48], [45, 50], [35, 50], [60, 63]];
const imageURLs = imageSizes.map(([w, h]) =>
`http://placekitten.com/${w}/${h}`
);
Promise.all(imageURLs.map(e =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e;
})
))
.then(initializeMJS)
;
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>

Fabric.js line rendering when resizing object

Have an issue with line rendering when resizing object.
I've locked line endings positions to exact point on circles and when moving, scaling, rotating etc I have to edit lines connected to current circle.
Here is fiddle
Just try to resize circles and at some point you'll see that rendering is crashed a bit which corresponds to lines. Need a help for it, maybe rerender or something.
Or that's an issue of fabric.js
var circlesData = [{
id: 1,
x: 80,
y: 80,
r: 60
}, {
id: 2,
x: 440,
y: 190,
r: 90
}];
var connectionsData = [{
from: {id: 1, angle: 0, rdist: .8},
to: {id: 2, angle: 0, rdist: .4},
}]
var fcircles = [];
var fconnections = [];
var fcanvas;
init();
function init() {
fcanvas = new fabric.Canvas('c', {
imageSmoothingEnabled: false,
allowTouchScrolling: true,
});
fcanvas.preserveObjectStacking = true;
fcanvas.selection = false;
fcanvas.setBackgroundColor('#fff');
fcircles = circlesData.map(function(circleData) {
var circle = new fabric.Circle({
left: circleData.x,
top: circleData.y,
radius: circleData.r,
fill: 'rgba(100,100,255,0.2)',
originX: 'center',
originY: 'center'
});
circle.initialData = circleData;
circle.setControlsVisibility({
mt: false,
mb: false,
ml: false,
mr: false,
mtr: false,
});
return circle;
});
fconnections = connectionsData.map(function(connectionData) {
var line = new fabric.Line([0,0,0,0], {
strokeWidth: 6,
strokeLineCap: 'round',
fill: 'red',
stroke: 'red',
originX: 'center',
originY: 'center'
});
line.from = copyJson(connectionData.from);
line.to = copyJson(connectionData.to);
line.selectable = false;
return line;
});
fcircles.concat(fconnections).forEach(function(fobj){
fcanvas.add(fobj)
});
updateConnections(fconnections);
fcanvas.renderAll();
console.log(fcanvas.getObjects())
fcanvas.on('object:moving', onObjChange);
fcanvas.on('object:scaling', onObjChange);
fcanvas.on('object:rotating', onObjChange);
}
function onObjChange(e) {
if(['line'].indexOf(e.target.type) > -1) {
return;
}
var circle = e.target;
updateConnections(fconnections.filter(function(fconnection){
return fconnection.from.id === e.target.initialData.id || fconnection.to.id === e.target.initialData.id;
}))
}
function updateConnections(fconnections) {
fconnections.forEach(function(fconnection) {
var from = fcircles.filter(function(c){return c.initialData.id === fconnection.from.id})[0];
var to = fcircles.filter(function(c){return c.initialData.id === fconnection.to.id})[0];
var fromAngle = fconnection.from.angle - from.angle / 180 * Math.PI;
var toAngle = fconnection.to.angle - from.angle / 180 * Math.PI;
debugger;
fconnection.set({
x1: from.left + fconnection.from.rdist * from.radius * Math.cos(fromAngle),
y1: from.top + fconnection.from.rdist * from.radius * Math.sin(fromAngle),
x2: to.left + fconnection.to.rdist * to.radius * Math.cos(toAngle),
y2: to.top + fconnection.to.rdist * to.radius * Math.sin(toAngle)
});
fconnection.setCoords();
});
}
function copyJson(obj) {
return JSON.parse(JSON.stringify(obj));
}
Add to your Line object property:
objectCaching: false
From fabricjs documentation:
objectCaching :Boolean When true, object is cached on an additional
canvas. default to true since 1.7.0

Multiple node selection in vis.js

I'm playing with vis.js because I like its Network Visualization module. I'd like to know, as I can't find it in documentation, if it's possibile to select multiple nodes.
Cheers,
Riccardo
Update!
the link for the documentation is http://visjs.org/docs/network/interaction.html
set the multiselect property to true.
add this section to your network option object.
interaction: { multiselect: true}
Search for "selectable" property in documentation at
http://visjs.org/docs/network/
If true, nodes in the network can be selected by clicking them. Long
press can be used to select multiple nodes.
If you are looking for a rectangle to select your nodes, just look at this thread on visjs-network github : https://github.com/almende/vis/issues/3594
Full demo code :
const nodes = new vis.DataSet([
{ id: 1, label: 'Node 1' },
{ id: 2, label: 'Node 2' },
{ id: 3, label: 'Node 3' },
{ id: 4, label: 'Node 4' },
{ id: 5, label: 'Node 5' }
]);
const edges = new vis.DataSet([
{ from: 1, to: 3 },
{ from: 1, to: 2 },
{ from: 2, to: 4 },
{ from: 2, to: 5 }
]);
const options = {
layout: { randomSeed: 2 },
interaction:{
hover: true,
multiselect: true
}
};
// Everything is in there
const makeMeMultiSelect = (container, network, nodes) => {
const NO_CLICK = 0;
const RIGHT_CLICK = 3;
// Disable default right-click dropdown menu
container[0].oncontextmenu = () => false;
// State
let drag = false, DOMRect = {};
// Selector
const canvasify = (DOMx, DOMy) => {
const { x, y } = network.DOMtoCanvas({ x: DOMx, y: DOMy });
return [x, y];
};
const correctRange = (start, end) =>
start < end ? [start, end] : [end, start];
const selectFromDOMRect = () => {
const [sX, sY] = canvasify(DOMRect.startX, DOMRect.startY);
const [eX, eY] = canvasify(DOMRect.endX, DOMRect.endY);
const [startX, endX] = correctRange(sX, eX);
const [startY, endY] = correctRange(sY, eY);
network.selectNodes(nodes.get().reduce(
(selected, { id }) => {
const { x, y } = network.getPositions(id)[id];
return (startX <= x && x <= endX && startY <= y && y <= endY) ?
selected.concat(id) : selected;
}, []
));
}
// Listeners
container.on("mousedown", function({ which, pageX, pageY }) {
// When mousedown, save the initial rectangle state
if(which === RIGHT_CLICK) {
Object.assign(DOMRect, {
startX: pageX - this.offsetLeft,
startY: pageY - this.offsetTop,
endX: pageX - this.offsetLeft,
endY: pageY - this.offsetTop
});
drag = true;
}
});
container.on("mousemove", function({ which, pageX, pageY }) {
// Make selection rectangle disappear when accidently mouseupped outside 'container'
if(which === NO_CLICK && drag) {
drag = false;
network.redraw();
}
// When mousemove, update the rectangle state
else if(drag) {
Object.assign(DOMRect, {
endX: pageX - this.offsetLeft,
endY: pageY - this.offsetTop
});
network.redraw();
}
});
container.on("mouseup", function({ which }) {
// When mouseup, select the nodes in the rectangle
if(which === RIGHT_CLICK) {
drag = false;
network.redraw();
selectFromDOMRect();
}
});
// Drawer
network.on('afterDrawing', ctx => {
if(drag) {
const [startX, startY] = canvasify(DOMRect.startX, DOMRect.startY);
const [endX, endY] = canvasify(DOMRect.endX, DOMRect.endY);
ctx.setLineDash([5]);
ctx.strokeStyle = 'rgba(78, 146, 237, 0.75)';
ctx.strokeRect(startX, startY, endX - startX, endY - startY);
ctx.setLineDash([]);
ctx.fillStyle = 'rgba(151, 194, 252, 0.45)';
ctx.fillRect(startX, startY, endX - startX, endY - startY);
}
});
}; // end makeMeMultiSelect
$(document).ready(() => {
const container = $("#network");
const network = new vis.Network(container[0], { nodes, edges }, options);
makeMeMultiSelect(container, network, nodes);
});
I've just discovered that in vis.js you can select multiple nodes long pressing them.

Categories

Resources