I'm using MatterJs for a physics based game and have not found a solution for the problem of preventing bodies being force-dragged by the mouse through other bodies. If you drag a body into another body, the body being dragged can force itself into and through the other body. I'm looking for a reliable way to prevent them from intersecting. You can observe this effect in any MatterJS demo by selecting a body with the mouse, and trying to force it through another body. Here is a typical example:
https://brm.io/matter-js/demo/#staticFriction
Unfortunately this breaks any games or simulations depending on drag-and-drop.
I have attempted numerous solutions, such as breaking the mouse constraint when a collision occurs, or reducing constraint stiffness, but nothing which works reliably.
Any suggestions welcome!
I think that the best answer here would be a significant overhaul to the Matter.Resolver module to implement predictive avoidance of physical conflicts between any bodies. Anything short of that is guaranteed to fail under certain circumstances. That being said here are two "solutions" which, in reality, are just partial solutions. They are outlined below.
Solution 1 (Update)
This solution has several advantages:
It is more concise than Solution 2
It creates a smaller computational footprint than Solution 2
The drag behavior is not interrupted the way it is in Solution 2
It can be non-destructively combined with Solution 2
The idea behind this approach is to resolve the paradox of what happens "when an unstoppable force meets an immovable object" by rendering the force stoppable. This is enabled by the Matter.Event beforeUpdate, which allows the absolute velocity and impulse (or rather positionImpulse, which isn't really physical impulse) in each direction to be constrained to within user-defined bounds.
window.addEventListener('load', function() {
var canvas = document.getElementById('world')
var mouseNull = document.getElementById('mouseNull')
var engine = Matter.Engine.create();
var world = engine.world;
var render = Matter.Render.create({ element: document.body, canvas: canvas,
engine: engine, options: { width: 800, height: 800,
background: 'transparent',showVelocity: true }});
var body = Matter.Bodies.rectangle(400, 500, 200, 60, { isStatic: true}),
size = 50, counter = -1;
var stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6,
0, 0, function(x, y) {
return Matter.Bodies.rectangle(x, y, size * 2, size, {
slop: 0, friction: 1, frictionStatic: Infinity });
});
Matter.World.add(world, [ body, stack,
Matter.Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(800, 300, 50, 600, { isStatic: true }),
Matter.Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
]);
Matter.Events.on(engine, 'beforeUpdate', function(event) {
counter += 0.014;
if (counter < 0) { return; }
var px = 400 + 100 * Math.sin(counter);
Matter.Body.setVelocity(body, { x: px - body.position.x, y: 0 });
Matter.Body.setPosition(body, { x: px, y: body.position.y });
if (dragBody != null) {
if (dragBody.velocity.x > 25.0) {
Matter.Body.setVelocity(dragBody, {x: 25, y: dragBody.velocity.y });
}
if (dragBody.velocity.y > 25.0) {
Matter.Body.setVelocity(dragBody, {x: dragBody.velocity.x, y: 25 });
}
if (dragBody.positionImpulse.x > 25.0) {
dragBody.positionImpulse.x = 25.0;
}
if (dragBody.positionImpulse.y > 25.0) {
dragBody.positionImpulse.y = 25.0;
}
}
});
var mouse = Matter.Mouse.create(render.canvas),
mouseConstraint = Matter.MouseConstraint.create(engine, { mouse: mouse,
constraint: { stiffness: 0.1, render: { visible: false }}});
var dragBody = null
Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
dragBody = event.body;
});
Matter.World.add(world, mouseConstraint);
render.mouse = mouse;
Matter.Engine.run(engine);
Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.js"></script>
In the example I am restricting the velocity and positionImpulse in x and y to a maximum magnitude of 25.0. The result is shown below
As you can see, it is possible to be quite violent in dragging the bodies and they will not pass through one another. This is what sets this approach apart from others: most other potential solutions fail when the user is sufficiently violent with their dragging.
The only shortcoming I have encountered with this method is that it is possible to use a non-static body to hit another non-static body hard enough to give it sufficient velocity to the point where the Resolver module will fail to detect the collision and allow the second body to pass through other bodies. (In the static friction example the required velocity is around 50.0, I've only managed to do this successfully one time, and consequently I do not have an animation depicting it).
Solution 2
This is an additional solution, fair warning though: it is not straightforward.
In broad terms the way this works is to check if the body being dragged, dragBody, has collided with a static body and if the mouse has since moved too far without dragBody following. If it detects that the separation between the mouse and dragBody has become too large it removes the Matter.js mouse.mousemove event listener from mouse.element and replaces it with a different mousemove function, mousemove(). This function checks if the mouse has returned to within a given proximity of the center of the body. Unfortunately I couldn't get the built-in Matter.Mouse._getRelativeMousePosition() method to work properly so I had to include it directly (someone more knowledgeable than me in Javascript will have to figure that one out). Finally, if a mouseup event is detected it switches back to the normal mousemove listener.
window.addEventListener('load', function() {
var canvas = document.getElementById('world')
var mouseNull = document.getElementById('mouseNull')
var engine = Matter.Engine.create();
var world = engine.world;
var render = Matter.Render.create({ element: document.body, canvas: canvas,
engine: engine, options: { width: 800, height: 800,
background: 'transparent',showVelocity: true }});
var body = Matter.Bodies.rectangle(400, 500, 200, 60, { isStatic: true}),
size = 50, counter = -1;
var stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6,
0, 0, function(x, y) {
return Matter.Bodies.rectangle(x, y, size * 2, size, {
slop: 0.5, friction: 1, frictionStatic: Infinity });
});
Matter.World.add(world, [ body, stack,
Matter.Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(800, 300, 50, 600, { isStatic: true }),
Matter.Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
]);
Matter.Events.on(engine, 'beforeUpdate', function(event) {
counter += 0.014;
if (counter < 0) { return; }
var px = 400 + 100 * Math.sin(counter);
Matter.Body.setVelocity(body, { x: px - body.position.x, y: 0 });
Matter.Body.setPosition(body, { x: px, y: body.position.y });
});
var mouse = Matter.Mouse.create(render.canvas),
mouseConstraint = Matter.MouseConstraint.create(engine, { mouse: mouse,
constraint: { stiffness: 0.2, render: { visible: false }}});
var dragBody, overshoot = 0.0, threshold = 50.0, loc, dloc, offset,
bodies = Matter.Composite.allBodies(world), moveOn = true;
getMousePosition = function(event) {
var element = mouse.element, pixelRatio = mouse.pixelRatio,
elementBounds = element.getBoundingClientRect(),
rootNode = (document.documentElement || document.body.parentNode ||
document.body),
scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset :
rootNode.scrollLeft,
scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset :
rootNode.scrollTop,
touches = event.changedTouches, x, y;
if (touches) {
x = touches[0].pageX - elementBounds.left - scrollX;
y = touches[0].pageY - elementBounds.top - scrollY;
} else {
x = event.pageX - elementBounds.left - scrollX;
y = event.pageY - elementBounds.top - scrollY;
}
return {
x: x / (element.clientWidth / (element.width || element.clientWidth) *
pixelRatio) * mouse.scale.x + mouse.offset.x,
y: y / (element.clientHeight / (element.height || element.clientHeight) *
pixelRatio) * mouse.scale.y + mouse.offset.y
};
};
mousemove = function() {
loc = getMousePosition(event);
dloc = dragBody.position;
overshoot = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5 - offset;
if (overshoot < threshold) {
mouse.element.removeEventListener("mousemove", mousemove);
mouse.element.addEventListener("mousemove", mouse.mousemove);
moveOn = true;
}
}
Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
dragBody = event.body;
loc = mouse.position;
dloc = dragBody.position;
offset = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5;
Matter.Events.on(mouseConstraint, 'mousemove', function(event) {
loc = mouse.position;
dloc = dragBody.position;
for (var i = 0; i < bodies.length; i++) {
overshoot = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5 - offset;
if (bodies[i] != dragBody &&
Matter.SAT.collides(bodies[i], dragBody).collided == true) {
if (overshoot > threshold) {
if (moveOn == true) {
mouse.element.removeEventListener("mousemove", mouse.mousemove);
mouse.element.addEventListener("mousemove", mousemove);
moveOn = false;
}
}
}
}
});
});
Matter.Events.on(mouseConstraint, 'mouseup', function(event) {
if (moveOn == false){
mouse.element.removeEventListener("mousemove", mousemove);
mouse.element.addEventListener("mousemove", mouse.mousemove);
moveOn = true;
}
});
Matter.Events.on(mouseConstraint, 'enddrag', function(event) {
overshoot = 0.0;
Matter.Events.off(mouseConstraint, 'mousemove');
});
Matter.World.add(world, mouseConstraint);
render.mouse = mouse;
Matter.Engine.run(engine);
Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.js"></script>
After applying the event listener switching scheme the bodies now behave more like this
I have tested this fairly thoroughly, but I can't guarantee it will work in every case. It also bears noting that the mouseup event is not detected unless the mouse is within the canvas when it occurs - but this is true for any Matter.js mouseup detection so I didn't try to fix that.
If the velocity is sufficiently large, Resolver will fail to detect any collision, and since it lacks predictive prevention of this flavor of physical conflict, will allow the body to pass through, as shown here.
This can be resolved by combining with Solution 1.
One last note here, it is possible to apply this to only certain interactions (e.g. those between a static and a non-static body). Doing so is accomplished by changing
if (bodies[i] != dragBody && Matter.SAT.collides(bodies[i], dragBody).collided == true) {
//...
}
to (for e.g. static bodies)
if (bodies[i].isStatic == true && bodies[i] != dragBody &&
Matter.SAT.collides(bodies[i], dragBody).collided == true) {
//...
}
Failed solutions
In case any future users come across this question and find both solutions insufficient for their use case, here are some of the solutions I attempted which did not work. A guide of sorts for what not to do.
Calling mouse.mouseup directly: object deleted immediately.
Calling mouse.mouseup via Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}): overridden by Engine.update, behavior unchanged.
Making the dragged object temporarily static: object deleted on returning to non-static (whether via Matter.Body.setStatic(body, false) or body.isStatic = false).
Setting the force to (0,0) via setForce when approaching conflict: object can still pass through, would need to be implemented in Resolver to actually work.
Changing mouse.element to a different canvas via setElement() or by mutating mouse.element directly: object deleted immediately.
Reverting object to last 'valid' position: still allows pass through,
Change behavior via collisionStart: inconsistent collision detection still permits pass through with this method
I would have managed the feature in another way:
No "drag" (so no continuos align of dragpoint with offset Vs dragged object)
On mouseDown the mouse pointer position give an oriented velocity vector for object to follow
On mouseUp reset your velocity vector
Let the matter simulation do the rest
To control collision when dragged you need to utilize collision filter and events.
Create bodies with default collision filter mask 0x0001. Add catch startdrag and enddrag events and set different body collision filter category to temporarily avoid collisions.
Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
event.body.collisionFilter.category = 0x0008; // move body to new category to avoid collision
});
Matter.Events.on(mouseConstraint, 'enddrag', function(event) {
event.body.collisionFilter.category = 0x0001; // return body to default category to activate collision
});
window.addEventListener('load', function () {
//Fetch our canvas
var canvas = document.getElementById('world');
//Setup Matter JS
var engine = Matter.Engine.create();
var world = engine.world;
var render = Matter.Render.create({
canvas: canvas,
engine: engine,
options: {
width: 800,
height: 800,
background: 'transparent',
wireframes: false,
showAngleIndicator: false
}
});
//Add a ball
const size = 50;
const stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6, 0, 0, (x, y) => {
return Matter.Bodies.rectangle(x, y, size * 2, size, {
collisionFilter: {
mask: 0x0001,
},
slop: 0.5,
friction: 1,
frictionStatic: Infinity,
});
});
Matter.World.add(engine.world, stack);
//Add a floor
var floor = Matter.Bodies.rectangle(250, 520, 500, 40, {
isStatic: true, //An immovable object
render: {
visible: false
}
});
Matter.World.add(world, floor);
//Make interactive
var mouseConstraint = Matter.MouseConstraint.create(engine, { //Create Constraint
element: canvas,
constraint: {
render: {
visible: false
},
stiffness: 0.8
}
});
Matter.World.add(world, mouseConstraint);
// add events to listen drag
Matter.Events.on(mouseConstraint, 'startdrag', function (event) {
event.body.collisionFilter.category = 0x0008; // move body to new category to avoid collision
});
Matter.Events.on(mouseConstraint, 'enddrag', function (event) {
event.body.collisionFilter.category = 0x0001; // return body to default category to activate collision
});
//Start the engine
Matter.Engine.run(engine);
Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.min.js"></script>
This seems to be related to issue 672 on their GitHub page which seems to suggest that this occurs due to a lack of Continuous Collision Detection (CCD).
An attempt to remedy this has been made and the code for it can be found here but the issue is still open so it looks like you might need to edit the engine to build CCD into it yourself.
So basically what I need to do is to load an SVG with SNAP.svg and add an effect (zoom in) I understand that in order to achieve this I need to respect this :
Load the SVG file
Scale de SVG in (0 ?)
Append this SVG
Add a transform effect (with the suitable scale)
The problem is that I need to display this in a 650 width and 280 height size.
The SVG I'm loading, witch I'll name it 'map' is in 1920 width and 1080 height.
This is my code so far :
<svg id="svg" width="650px" height="280px"></svg>
<script type="text/javascript">
var s = Snap("#svg");
var map = Snap.load("./src/map.svg", function (f) {
g = f.select("g");
var t = Snap.matrix().scale(0.35);
s.append(g);
g.group(g.selectAll("path")).transform(t);
});
</script>
It seems the scale instruction is working find but not the animation.
Also, how I can center this loaded SVG not matter what scale it takes ?
Thank you !
UPDATE :
I managed to add some effects but I don't think the way I'm doing it it's the correct one :
var carte = Snap.load("./src/carte.svg", function (f) {
g = f.select("g");
//var t = Snap.matrix().scale(0.35);
s.append(g);
//Set the map in first position
var firstScene = new Snap.Matrix();
firstScene.translate(300, 160);
firstScene.scale(0.05);
//Zoom effect
var secondScene = new Snap.Matrix();
secondScene.scale(2.0);
secondScene.translate(-850, -360);
//Move the matrix till desired point (not finish)
var threeScene = new Snap.Matrix();
threeScene.translate(-850, -360);
g.animate({ transform: firstScene }, 0, function() {g.animate ({ transform: secondScene}, 1500, mina.linear )});
});
It seems impossible to add a timer or more than two effects ?
Just as an alternative if there are quite a few sequenced animations I may be tempted to write a function to handle an array of animations. It could look something like this...
function nextFrame ( el, frameArray, whichFrame ) {
if( whichFrame >= frameArray.length ) { return }
el.animate( frameArray[ whichFrame ].animation,
frameArray[ whichFrame ].dur,
frameArray[ whichFrame ].easing,
nextFrame.bind( null, el, frameArray, whichFrame + 1 ) );
}
var block = s.rect(100, 100, 100, 100, 20, 20);
.attr({ fill: "rgb(236, 240, 241)", stroke: "#1f2c39",
strokeWidth: 3, transform: 's1' });
var frames = [
{ animation: { transform: 's0.4,0.4,200,200' }, dur: 1000, easing: mina.bounce },
{ animation: { transform: 't-100,-80' }, dur: 1000, easing: mina.bounce },
{ animation: { transform: 's1.2,1.2,300,300t200,-100' },dur: 1000, easing: mina.bounce }
];
nextFrame( block, frames, 0 );
jsfiddle
I think if you want to sequence animations, the most elegant way would be to use promises. For that all you would need is to wrap animate function in Q library or jQuery.Deferred. Here is the example I put together on jsFiddle https://jsfiddle.net/stsvilik/Lnhc77b2/
function animate(obj, conf, duration, asing) {
var def = Q.defer();
obj.animate(conf, dur, easing, function(){
def.resolve();
});
return def.promise;
}
This seems to work fine, but like lan said above, maybe his method it's better for doing animations
var carte = Snap.load("./src/carte.svg", function (f) {
g = f.select("g");
//var t = Snap.matrix().scale(0.35);
s.append(g);
//Load the map
var firstScene = new Snap.Matrix();
firstScene.translate(295, 160);
firstScene.scale(0.04);
//Set scene 1
var secondScene = new Snap.Matrix();
secondScene.scale(0.4);
secondScene.translate(-300, -10);
//Set scene 2
var threeScene = new Snap.Matrix();
//threeScene.scale(0.5);
threeScene.translate(-825, -380);
//Set scene 3
var fourScene = new Snap.Matrix();
fourScene.scale(21.0);
fourScene.translate(-1164, -526);
var anim1 = function() {
g.animate({ transform: firstScene}, 0, anim2);
}
var anim2 = function() {
g.animate({ transform: secondScene}, 1500, mina.easing, anim3);
}
var anim3 = function() {
g.animate({ transform: threeScene}, 1000, mina.linear, anim4);
}
var anim4 = function() {
g.animate({ transform: fourScene}, 2000, mina.easing);
}
anim1();
});
Is the fact of adding several matrix a performance killer ? Or this is the way it should be done ?
I'm searching for a good vertical bubble marquee plugin.
Not simple vertical marquee, I'm looking for a good "flash like" effects plugin, something smooth with element marquee from bottom to top of a div content.
Could be really nice but I think it's only in my dreams this plugin
Well, it's not terribly efficient, but this is a good start I think:
jQuery.fn.verticalMarquee = function(vertSpeed, horiSpeed) {
this.css('float', 'left');
vertSpeed = vertSpeed || 1;
horiSpeed = 1/horiSpeed || 1;
var windowH = this.parent().height(),
thisH = this.height(),
parentW = (this.parent().width() - this.width()) / 2,
rand = Math.random() * 1000,
current = this;
this.css('margin-top', windowH + thisH);
this.parent().css('overflow', 'hidden');
setInterval(function() {
current.css({
marginTop: function(n, v) {
return parseFloat(v) - vertSpeed;
},
marginLeft: function(n, v) {
return (Math.sin(new Date().getTime() / (horiSpeed * 1000) + rand) + 1) * parentW;
}
});
}, 15);
setInterval(function() {
if (parseFloat(current.css('margin-top')) < -thisH) {
current.css('margin-top', windowH + thisH);
}
}, 250);
};
$('.message').verticalMarquee(0.5, 1);
It uses Math.sin to move the element horizontally. The function verticalMarquee accepts two arguments, one for vertical speed and the other for horizontal speed. The function can only be called on jQuery objects that contains only one element - during testing anything more than one element been animated at once caused terrible amount of lagging.
See a simple demo here: http://jsfiddle.net/CcccQ/2/
Do you mean something like The Silky Smooth Marquee plugin?