Particle class
class Point {
constructor(x, y, vx, vy, radius) {
this.pos = new Phaser.Math.Vector2(x, y);
this.oldpos = new Phaser.Math.Vector2(x + (vx||0), y + (vy||0)); // velocity x, y
this.bounce = 0.99;
this.friction = 0.97;
this.groundFriction = 0.7;
this.gravity = new Phaser.Math.Vector2(0, 1);
this.pinned = false;
this.radius = radius || 5;
this.color = "0xe62a4f";
this.mass = 1;
}
update() {
if (this.pinned) return;
let vel = this.pos.clone().subtract(this.oldpos);
vel.scale(this.friction);
// if the point touches the ground set groundFriction
if (this.pos.y >= CANVAS_HEIGHT - this.radius && vel.lengthSq() > 0.000001) {
vel.scale(this.groundFriction);
}
this.oldpos.set(this.pos.x, this.pos.y);
this.pos.add(vel);
this.pos.add(this.gravity);
}
resetVelocity() {
this.oldpos.set(this.pos.x, this.pos.y);
}
setGravity(g) {
this.gravity = g;
return this;
}
setFriction(f) {
this.friction = f;
return this;
}
setGroundFriction(f) {
this.groundFriction = f;
return this;
}
setVelocity(vel) {
this.oldpos.set(vel.x, vel.y);
return this;
}
setBounce(b) {
this.bounce = b;
return this;
}
pin() {
this.pinned = true;
return this;
}
unpin() {
this.pinned = false;
return this;
}
constrain() {
if (this.pos.x > CANVAS_WIDTH - this.radius) {
this.pos.x = CANVAS_WIDTH - this.radius;
}
if (this.pos.x < this.radius) {
this.pos.x = this.radius;
}
if (this.pos.y > CANVAS_HEIGHT - this.radius) {
this.pos.y = CANVAS_HEIGHT - this.radius;
}
if (this.pos.y < this.radius) {
this.pos.y = this.radius;
}
};
render(ctx) {
ctx.fillStyle(this.color).fillCircle(this.pos.x, this.pos.y, this.radius)
}
}
Distance Constraint
class Stick {
constructor(p1, p2, length) {
this.startPoint = p1;
this.endPoint = p2;
this.stiffness = 2;
this.color = '0xf5476a';
// if the length is not given then calculate the distance based on position
if (!length) {
this.length = Phaser.Math.Distance.BetweenPoints(this.startPoint.pos, this.endPoint.pos);
} else {
this.length = length;
}
}
update() {
// calculate the distance between two dots
let dx = this.endPoint.pos.x - this.startPoint.pos.x;
let dy = this.endPoint.pos.y - this.startPoint.pos.y;
// pythagoras theorem
let dist = Math.sqrt(dx dx + dy dy);
// calculate the resting distance betwen the dots
let diff = (this.length - dist) / dist * this.stiffness;
// getting the offset of the points
let offsetx = dx diff 0.5;
let offsety = dy diff 0.5;
// calculate mass
let m1 = this.startPoint.mass + this.endPoint.mass;
let m2 = this.startPoint.mass / m1;
m1 = this.endPoint.mass / m1;
// and finally apply the offset with calculated mass
if (!this.startPoint.pinned) {
this.startPoint.pos.x -= offsetx * m1;
this.startPoint.pos.y -= offsety * m1;
}
if (!this.endPoint.pinned) {
this.endPoint.pos.x += offsetx * m2;
this.endPoint.pos.y += offsety * m2;
}
}
render(ctx) {
ctx.lineStyle(1,this.color);
ctx.lineBetween(this.startPoint.pos.x, this.startPoint.pos.y, this.endPoint.pos.x, this.endPoint.pos.y);
}
}
Entity Class
class Entity {
constructor(scene, iterations) {
this.scene = scene;
this.points = [];
this.sticks = [];
this.iterations = iterations || 16;
}
addPoint(x, y, vx, vy) {
this.points.push(new Point(x, y,vx, vy));
}
addStick(p1, p2) {
this.sticks.push(new Stick(this.points[p1], this.points[p2]));
}
setGravity(g) {
for (let i = 0; i < this.points.length; i++) {
this.points[i].setGravity(g);
}
}
pin(index) {
this.points[index].pin();
}
updatePoints() {
for (let i = 0; i < this.points.length; i++) {
this.points[i].update();
}
}
updateSticks() {
for (let i = 0; i < this.sticks.length; i++) {
this.sticks[i].update();
}
}
update() {
this.updatePoints();
for (let j = 0; j < this.iterations; j++) {
this.updateSticks();
this.updateContrains();
}
}
render() {
this.renderPoints();
this.renderSticks();
}
updateContrains() {
for (let i = 0; i < this.points.length; i++) {
this.points[i].constrain();
}
}
renderPoints() {
for (let i = 0; i < this.points.length; i++) {
this.points[i].render(this.graphics);
}
}
renderSticks() {
for (let i = 0; i < this.sticks.length; i++) {
this.sticks[i].render(this.graphics);
}
}
}
Main Simulation class
class Verly {
constructor(scene, iterations) {
this.scene = scene;
this.entities = [];
this.iterations = iterations;
this.mouse = new Mouse(this.scene, this.entities)
}
addEntity(e) {
this.entities.push(e);
}
update() {
this.graphics.clear();
for (let i = 0; i < this.entities.length; i++) {
this.entities[i].update();
}
}
render() {
for (let i = 0; i < this.entities.length; i++) {
this.entities[i].render();
}
}
createBox(x, y, w, h) {
const box = new Entity(this.scene, this.iterations, "Box");
box.addPoint(x, y, 0, 0);
box.addPoint(x + w, y, 0, 0);
box.addPoint(x + w, y + h, 0, 0);
box.addPoint(x, y + h, 0, 0);
box.addStick(0, 1);
box.addStick(1, 2);
box.addStick(2, 3);
box.addStick(3, 0);
box.addStick(3, 1);
this.addEntity(box);
return box;
}
createRope(x, y, segments = 10, gap = 15, pin) {
let rope = new Entity(this.scene, this.iterations, this, 'Rope');
for (let i = 0; i < segments; i++) {
rope.addPoint(x + i * gap, y, 0, 0);
}
for (let i = 0; i < segments - 1; i++) {
rope.addStick(i, (i + 1) % segments);
}
if (pin !== undefined) {
rope.pin(pin);
}
this.addEntity(rope);
return rope;
}
interact() {
this.mouse.drag();
}
}
class Mouse {
constructor(scene, entities) {
this.scene = scene;
this.entities = entities;
// mouse related variables
this.draggedPoint = null;
this.down = false;
this.coord = new Phaser.Math.Vector2()
this.text = this.scene.add.text(0,0,'', {color: '0x000000'})
this.scene.input.on('pointermove', pointer => this.interact(pointer));
this.scene.input.on('pointerdown', pointer => {
this.down = true;
this.drag()
});
this.scene.input.on('pointerup', pointer=>{
if (this.draggedPoint) {
this.draggedPoint.resetVelocity();
};
this.down = false;
this.draggedPoint = null;
})
}
interact(pointer) {
this.coord.copy(pointer)
this.text.setText("x="+this.coord.x+"y="+this.coord.y);
}
dragPoint() {
if (!this.down) return;
this.draggedPoint.pos.set(this.coord.x, this.coord.y);
}
drag() {
if (!this.down) {
this.draggedPoint = this.getNearestPoint();
}
if (this.draggedPoint) {
this.renderDraggedPoint(this.draggedPoint);
this.dragPoint();
}
}
renderDraggedPoint(point) {
this.graphics.lineStyle(1,'0x000000');
this.graphics.strokeCircle(point.pos.x, point.pos.y, 8);
}
getNearestPoint() {
// if (!this.down) return false;
let d = 20;
let p = null;
for (let k = 0; k < this.entities.length; k++) {
for (let i = 0; i < this.entities[k].points.length; i++) {
let dist = Phaser.Math.Distance.BetweenPoints(this.entities[k].points[i].pos, this.coord);
if (dist < d) {
p = this.entities[k].points[i];
}
}
}
return p;
}
}
Boot
class main extends Phaser.Scene {
constructor() {
super({ key: "main" });
}
preload() {}
create() {
this.world = new Verly(this);
this.world.createBox(100,100,100,100)
this.world.createRope(100,50,10,15,0)
}
update() {
this.world.update();
this.world.render();
this.world.interact();
}
}
Useful References
Comments