I'm trying to build a screen with circular bodies (bubbles). The bubbles should be able to change the scale with some interval, e.g. every 3 seconds and random value (from 1 to 3). The scale should change the size of a bubble and its mass.
Calling
Matter.Body.scale
doesn't make any effect. Bubbles keep staying the same. Maybe because I'm not using sprites with textures, but the Game-Engine entities with renderer as React.PureComponent instead. Not sure
Bubble node:
export interface BubbleProps {
body: Matter.Body;
item: Item;
}
class BubbleNode extends React.Component<BubbleProps> {
render() {
const {body, item} = this.props;
const x = body.position.x - body.circleRadius;
const y = body.position.y - body.circleRadius;
const style: ViewStyle = {
position: 'absolute',
left: x,
top: y,
width: body.circleRadius * 2,
height: body.circleRadius * 2,
backgroundColor: item.color,
borderRadius: body.circleRadius,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
padding: 3,
};
return <View style={style}>{item.content}</View>;
}
}
Bubble Entity:
const body = Matter.Bodies.circle(
randomX,
randomY,
RADIUS, //default radius 25
{
mass: 30,
}
);
return {
body,
item,
renderer: BubbleNode,
};
Update method:
// find out if the entity was already added to the world
if (Object.prototype.hasOwnProperty.call(entities, id)) {
//update scale
const scale = bubble.item.scale
Matter.Body.scale(bubble.body, scale, scale)
} else {
//add new entity
entities[id] = bubble;
Matter.World.add(physics.world, [bubble.body]);
}
Also, I need to make this scaling smooth
Edit:
Interesting thing. I was able to scale the bubbles using Matter.Body.scale but right before adding to the world. I wonder if there is a method to update the bodies after adding to the world.
Final edit
this is the method I'm using to set up the world and specify entities for the GameEngine framework
private setupWorld = (layout: LayoutRectangle) => {
const engine = Matter.Engine.create({enableSleeping: false});
const world = engine.world;
world.gravity.x = 0;
world.gravity.y = 0;
//center gravity body
Matter.use(MatterAttractors);
var attractiveBody = Matter.Bodies.circle(
layout.width / 2,
layout.height / 2,
1,
{
isSensor: true,
plugin: {
attractors: [centerGravity],
},
},
);
Matter.World.add(world, attractiveBody);
//walls
const wallThickness = 5;
let floor = Matter.Bodies.rectangle(
layout.width / 2,
layout.height - wallThickness / 2,
layout.width,
wallThickness,
{isStatic: true},
);
let ceiling = Matter.Bodies.rectangle(
layout.width / 2,
wallThickness / 2,
layout.width,
wallThickness,
{isStatic: true},
);
let left = Matter.Bodies.rectangle(
wallThickness / 2,
layout.height / 2,
wallThickness,
layout.height,
{isStatic: true},
);
let right = Matter.Bodies.rectangle(
layout.width - wallThickness / 2,
layout.height / 2,
wallThickness,
layout.height,
{isStatic: true},
);
Matter.World.add(world, [floor, ceiling, left, right]);
//basic entitites
this.entities = {
physics: {engine, world},
floor: {body: floor, color: 'green', renderer: Wall},
ceiling: {body: ceiling, color: 'green', renderer: Wall},
left: {body: left, color: 'green', renderer: Wall},
right: {body: right, color: 'green', renderer: Wall},
};
};
And here is the method that will be triggered by a parent Component with some interval
public updateNodes = (items: Item[]) => {
if (!this.state.mounted || !this.entities || !this.entities.physics || !this.layout) {
console.log('Missiing required data');
return;
}
const layout = this.layout
const entities = this.entities
const bubbles: BubbleEntity[] = items.map((item) => {
const randomX = randomPositionValue(layout.width);
const randomY = randomPositionValue(layout.height);
const body = Matter.Bodies.circle(
randomX,
randomY,
RADIUS, {
mass: 30,
}
);
body.label = item.id
return {
body,
item,
renderer: BubbleNode,
};
});
const physics = this.entities.physics as PhysicsEntity;
const allBodies = Matter.Composite.allBodies(physics.world)
bubbles.forEach((bubble) => {
//update existing or add new
const id = `bubble#${bubble.item.id}`;
if (Object.prototype.hasOwnProperty.call(entities, id)) {
//do physical node update here
//update scale and mass
const scale = bubble.item.scale
console.log('Updating item', id, scale);
//right her there used to be an issue because I used **bubble.body** which was not a correct reference to the world's body.
//so when I started to use allBodies array to find a proper reference of the body, everything started to work
let body = allBodies.find(item => item.label === bubble.item.id)
if (!!body) {
const scaledRadius = RADIUS*scale
const current = body.circleRadius || RADIUS
const scaleValue = scaledRadius/current
Matter.Body.scale(body, scaleValue, scaleValue)
}else{
console.warn('Physycal body not found, while the entity does exist');
}
} else {
console.log('Adding entity to the world');
entities[id] = bubble;
Matter.World.add(physics.world, [bubble.body]);
}
});
this.entities = entities
};
In the future, I'm going to improve that code, I will use some variables for the body and will create a matter.js plugin that will allow me to scale the body smoothly and not instant as it works right now. Also, the method above requires some clean, short implementation instead of that garbage I made attempting to make it work
Your example isn't exactly complete; it's not clear how (or if) the MJS engine is running. The first step is to make sure you have an actual rendering loop using calls to Matter.Engine.update(engine);
in a requestAnimationFrame
loop.
React doesn't seem critical here. It shouldn't affect the result since x/y coordinates and radii for each body in the MJS engine are extracted and handed to the View component, so the data flows in one direction. I'll leave it out for the rest of this example but it should be easy to reintroduce once you have the MJS side working to your satisfaction.
The way to scale a body in MJS is to call Matter.body.scale(body, scaleX, scaleY)
. This function recomputes other physical properties such as mass for the body.
There's an annoying caveat with this function: instead of setting an absolute scale as a JS canvas context or CSS transformation might, it sets a relative scale. This means each call to this function changes the baseline scaling for future calls. The problem with this is that rounding errors can accumulate and drift can occur. It also makes applying custom tweens difficult.
Workarounds are likely to be dependent on what the actual animation you hope to achieve is (and may not even be necessary), so I'll avoid prescribing anything too specific other than suggesting writing logic relative to the radius as the point of reference to ensure it stays within bounds. Other workarounds can include re-creating and scaling circles per frame.
Another gotcha when working with circles is realizing that MJS circles are n-sided polygons, so small circles lose resolution when scaled up. Again, this is use-case dependent, but you may wish to create a Bodies.polygon
with more sides than would be created by Bodies.circle
if you experience unusual behavior.
That said, here's a minimal, complete example of naive scaling that shows you how you can run an animation loop and call scale
to adjust it dynamically over time. Consider it a proof of concept and will require adaptation to work for your use case (whatever that may be).
const engine = Matter.Engine.create();
const circles = [...Array(15)].map((_, i) => {
const elem = document.createElement("div");
elem.classList.add("circle");
document.body.append(elem);
const body = Matter.Bodies.circle(
// x, y, radius
10 * i + 60, 0, Math.random() * 5 + 20
);
return {
elem,
body,
offset: i,
scaleAmt: 0.05,
speed: 0.25,
};
});
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: document.body}
);
const walls = [
Matter.Bodies.rectangle(
// x, y, width, height
innerWidth / 2, 0, innerWidth, 40, {isStatic: true}
),
Matter.Bodies.rectangle(
innerWidth / 2, 180, innerWidth, 40, {isStatic: true}
),
Matter.Bodies.rectangle(
0, innerHeight / 2, 40, innerHeight, {isStatic: true}
),
Matter.Bodies.rectangle(
300, innerHeight / 2, 40, innerHeight, {isStatic: true}
),
];
Matter.Composite.add(
engine.world,
[mouseConstraint, ...walls, ...circles.map(e => e.body)]
);
/*
naive method to mitigate scaling drift over time.
a better approach would be to scale proportional to radius.
*/
const baseScale = 1.00063;
(function update() {
requestAnimationFrame(update);
circles.forEach(e => {
const {body, elem} = e;
const {x, y} = body.position;
const {circleRadius: radius} = body;
const scale = baseScale + e.scaleAmt * Math.sin(e.offset);
Matter.Body.scale(body, scale, scale);
e.offset += e.speed;
elem.style.top = `${y - radius / 2}px`;
elem.style.left = `${x - radius / 2}px`;
elem.style.width = `${2 * radius}px`;
elem.style.height = `${2 * radius}px`;
elem.style.transform = `rotate(${body.angle}rad)`;
});
Matter.Engine.update(engine);
})();
* {
margin: 0;
padding: 0;
}
body, html {
height: 100%;
}
.circle {
border-radius: 50%;
position: absolute;
cursor: move;
background: rgb(23, 0, 36, 1);
background: linear-gradient(
90deg,
rgba(23, 0, 36, 1) 0%,
rgba(0, 212, 255, 1) 100%
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>