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.
Related
I've created a slingshot game in matter.js where I've created a ball which is used as a sling and some target balls on a ground which should be pushed off. I've attached the code below. I need help in tracking score so that everytime time user pushes all the balls off the ground "You won!" message should be displayed. Also need help in adding reset button. Link to the live game
let engine = Matter.Engine.create();
let render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: 1600,
height: 800,
wireframes: false
}
});
let ground = Matter.Bodies.rectangle(1200, 500, 300, 60, { isStatic: true });
let mouse = Matter.Mouse.create(render.canvas);
let mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
render: {visible: false}
}
});
render.mouse = mouse;
let ball = Matter.Bodies.circle(300, 600, 20);
let sling = Matter.Constraint.create({
pointA: {x:300, y:600},
bodyB: ball,
stiffness: 0.05
})
let stack = Matter.Composites.stack(1100, 270, 4, 4, 0, 0, function(x, y){
return Matter.Bodies.polygon(x, y, 8, 30);
});
let firing = false;
Matter.Events.on(mouseConstraint, 'enddrag', function(e){
if(e.body === ball) firing = true;
});
Matter.Events.on(engine, 'afterUpdate', function(){
if(firing && Math.abs(ball.position.x-300)<200 && Math.abs(ball.position.y-600)<20){
ball = Matter.Bodies.circle(300, 600, 20);
Matter.World.add(engine.world, ball);
sling.bodyB = ball;
firing = false;
}
})
Matter.World.add(engine.world, [stack, ground,ball, sling, mouseConstraint]);
Matter.Engine.run(engine);
Matter.Render.run(render);
Here is such a code snippet:
var game = new Phaser.Game(800, 600, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create });
var emitter;
function preload() {
game.load.image('wasp', 'assets/glass.png');
game.load.image('glass', 'assets/glass.png');
game.load.image('water', 'assets/blue-raster-floor.png');
}
function create() {
game.physics.startSystem(Phaser.Physics.ARCADE);
game.add.tileSprite(0, 344, 800, 256, 'water');
emitter = game.add.emitter(game.world.centerX, 200);
emitter.makeParticles('glass');
emitter.setXSpeed(-200, 200);
emitter.setYSpeed(-150, -250);
emitter.bringToTop = true;
emitter.setAlpha(0.1, 1, 500);
emitter.setScale(-2, 2, 1, 1, 3000, Phaser.Easing.Sinusoidal.InOut, true);
emitter.gravity = 300;
emitter.start(false, 5000, 700, 50);
game.time.events.add(3000, destroyEmitter, this);
}
function tweens(cash) {
var bugs;
var index = 0;
var data;
var pos = [];
var tween;
var tweenData = { x: 0, y: 0 };
tween = game.make.tween(tweenData).to( { x: 100, y: 400 }, 2000, "Sine.easeInOut");
tween.yoyo(true);
data = tween.generateData(60);
bugs = game.add.group();
pos.push(new Phaser.Point(32, 0));
pos.push(new Phaser.Point(300, 100));
pos.push(new Phaser.Point(600, 70));
bugs.create(pos[0].x, pos[0].y, 'wasp');
bugs.create(pos[1].x, pos[1].y, 'wasp');
bugs.create(pos[2].x, pos[2].y, 'wasp');
tween.onUpdateCallback(function () {
bugs.getAt(0).x = pos[0].x + data[index].x;
bugs.getAt(0).y = pos[0].y + data[index].y;
bugs.getAt(1).x = pos[1].x + (data[index].x / 2);
bugs.getAt(1).y = pos[1].y + data[index].y;
// Inverse one of the values
bugs.getAt(2).x = pos[2].x - data[index].x;
bugs.getAt(2).y = pos[2].y + data[index].y;
index++;
if (index === data.length)
{
index = 0;
}
});
tween.start();
}
function destroyEmitter() {
console.log(emitter);
emitter.destroy();
tweens();
}
As you can see, I have made the particle-animation. Such steps need to be taken:
Particle-animation should be cached in the form of a set of shots (textures)
Particle-animation should be deleted. I have already done it (by means of ‘destroy‘)
Instead of the particle animation sprite animation should be realized by means of the function tweens using received textures
and passing these textures as the argument of the function tweens
Any refractoring is welcome.
In Phaser, the emitter particles are of the relatively simple DisplayObject class which do not support animations like the Phaser.Sprite does. Btw I don't know if using tweens is the best way to animate particles, because I suspect it will be heavy on CPU usage, using Sprite animations on the other hand is a bit more "light weight".
But either way, you could create a custom particle class which contains the code for your particle animation (using tweens, animations, timers, whatever) and then set that custom class as the emitter.particleClass, see a code example in link below:
http://codetuto.com/2016/02/phaser-animated-particles/
I am trying to follow this tutorial here https://www.smashingmagazine.com/2012/10/design-your-own-mobile-game/ and I am stuck on the second part. (2. A Blank Canvas)
I am not sure where to put the POP.Draw object. Does it go inside of the var POP{} brackets where the other objects are created? I've tried keeping it inside, outside, and in the init function which I don't think makes sense. The purpose is to create methods within the new Draw object so they can be called later to create pictures in the canvas.
Here is my current code. It is the same as the one in the link:
var POP = {
//setting up initial values
WIDTH: 320,
HEIGHT: 480,
// we'll set the rest of these
//in the init function
RATIO: null,
currentWidth: null,
currentHeight: null,
canvas: null,
ctx: null,
init: function() {
//the proportion of width to height
POP.RATIO = POP.WIDTH / POP.HEIGHT;
//these will change when the screen is resized
POP.currentWidth = POP.WIDTH;
POP.currentHeight = POP.HEIGHT;
//this is our canvas element
POP.canvas = document.getElementsByTagName('canvas')[0];
//setting this is important
//otherwise the browser will
//default to 320x200
POP.canvas.width = POP.WIDTH;
POP.canvas.width = POP.HEIGHT;
//the canvas context enables us to
//interact with the canvas api
POP.ctx = POP.canvas.getContext('2d');
//we need to sniff out Android and iOS
// so that we can hide the address bar in
// our resize function
POP.ua = navigator.userAgent.toLowerCase();
POP.android = POP.ua.indexOf('android') > -1 ? true : false;
POP.ios = (POP.ua.indexOf('iphone') > -1 || POP.ua.indexOf('ipad') > -1) ? true : false;
//we're ready to resize
POP.resize();
POP.Draw.clear();
POP.Draw.rect(120, 120, 150, 150, 'green');
POP.Draw.circle(100, 100, 50, 'rgba(225,0,0,0.5)');
POP.Draw.text('Hello WOrld', 100, 100, 10, "#000");
},
resize: function() {
POP.currentHeight = window.innerHeight;
//resize the width in proportion to the new height
POP.currentWidth = POP.currentHeight * POP.RATIO;
//this will create some extra space on the page
//allowing us to scroll past the address bar thus hiding it
if (POP.android || POP.ios) {
document.body.style.height = (window.innerHeight + 50) + 'px';
}
//set the new canvas style width and height note:
//our canvas is still 320 x 400 but we're essentially scaling it with css
POP.canvas.style.width = POP.currentWidth + 'px';
POP.canvas.style.height = POP.currentHeight + 'px';
//we use a timeout here because some mobile browsers
//don't fire if there is not a short delay
window.selfTimeout(function() {
window.scrollTo(0, 1);
})
//this will create some extra space on the page
//enabling us to scroll past the address bar
//thus hiding it
if (POP.android || POP.ios) {
document.body.style.height = (window.innerHeight + 50) + 'px';
}
}
};
window.addEventListener('load', POP.init, false);
window.addEventListener('resize', POP.resize, false);
//abstracts various canvas operations into standalone functions
POP.Draw = {
clear: function() {
POP.ctx.clearRect(0, 0, POP.WIDTH, POP.HEIGHT);
},
rect: function(x, y, w, h, col) {
POP.ctx.fillStyle = col;
POP.ctx.fillRect(x, y, w, h);
},
circle: function(x, y, r, col) {
POP.ctx.fillStyle = col;
POP.ctx.beginPath();
POP.ctx.arc(x + 5, y + 5, r, 0, Math.PI * 2, true);
POP.ctx.closePath();
POP.ctx.fill();
},
text: function(string, x, y, size, col) {
POP.ctx.font = 'bold' + size + 'px Monospace';
POP.ctx.fillStyle = col;
POP.ctx.fillText(string, x, y);
}
};
SOLVED
I didn't realize but the completed code is on the webpage. I downloaded it and looked at the example for answers.
I solved the issue by placing the POP.Draw.clear, POP.Draw.rect methods before calling the POP.resize() method. I'm not really sure why the order matters, but it does.
I understand Fabric.js have built-in support on drag-n-drop within the same canvas.
How can we make it work for multiple canvas?
Or from an non-canvas html element e.g. an image from a table?
Drag and drop between canvases is possible in Fabric.js, but involves some manipulation of private properties. For this reason it is not guaranteed to be functional with future versions of Fabric.js.
Working demo: https://jsfiddle.net/mmalex/kdbu9f3y/
Video capture: https://youtu.be/nXZgCmIrpqQ
Key features:
✓ can drag and drop between any number of canvases (not only two),
✓ can drag back and forth between canvases without interruption,
✓ can transform (mirror) dropped image without interruption of manipulation.
Step 1 – prepare canvases, load images, arrange everything for demo:
//create two canvases
var canvas0El = document.getElementById("c0");
canvas0El.width = canvas0El.offsetWidth;
canvas0El.height = canvas0El.parentElement.offsetHeight;
var canvas1El = document.getElementById("c1");
canvas1El.width = canvas1El.offsetWidth;
canvas1El.height = canvas1El.parentElement.offsetHeight;
var canvas0 = new fabric.Canvas('c0');
canvas0.setBackgroundColor('rgba(19, 19, 19, 0.25)');
canvas0.renderAll();
var canvas1 = new fabric.Canvas('c1');
canvas1.setBackgroundColor('rgba(92, 18, 18, 0.25)');
canvas1.renderAll();
// add loaded image on left canvas
var onImageLoaded = function(oImg) {
oImg.originX = "center";
oImg.originY = "center";
oImg.left = this.x;
oImg.top = this.y;
canvas0.add(oImg);
oImg.canvas = canvas0;
imgArrow = oImg;
};
var config = { crossOrigin: 'anonymous' };
var baseUrl = "http://mbnsay.com/rayys/images";
var url0 = baseUrl + "/arrow-right-green.png";
var url1 = baseUrl + "/arrow-right-icon.png";
var url2 = baseUrl + "/arrow-right-blue.png";
// load some images
fabric.Image.fromURL(url0, onImageLoaded.bind({ x: 56, y: 96 }), config);
fabric.Image.fromURL(url0, onImageLoaded.bind({ x: 156, y: 96 }), config);
fabric.Image.fromURL(url1, onImageLoaded.bind({ x: 56, y: 2*96 }), config);
fabric.Image.fromURL(url1, onImageLoaded.bind({ x: 156, y: 2*96 }), config);
fabric.Image.fromURL(url2, onImageLoaded.bind({ x: 56, y: 3*96 }), config);
fabric.Image.fromURL(url2, onImageLoaded.bind({ x: 156, y: 3*96 }), config);
Step 2 – subscribe object:moving events on both canvases, and watch when object center crossing the canvas border. When object crosses the border, it has to be
remove from source canvas,
paste into destination canvas,
migrate internal canvas transformations (will be explained separately)
var onObjectMoving = function(p) {
var viewport = p.target.canvas.calcViewportBoundaries();
if (p.target.canvas === canvas0) {
if (p.target.left > viewport.br.x) {
console.log("Migrate: left -> center");
migrateItem(canvas0, canvas1, p.target);
return;
}
}
if (p.target.canvas === canvas1) {
if (p.target.left < viewport.tl.x) {
console.log("Migrate: center -> left");
migrateItem(canvas1, canvas0, p.target);
return;
}
}
};
canvas0.on("object:moving", onObjectMoving);
canvas1.on("object:moving", onObjectMoving);
Step 3 – The core of the solution, migrate object between canvases not interrupting mouse manipulations. Hard to explain, just follow the comments in code.
var migrateItem = function(fromCanvas, toCanvas, pendingImage) {
// Just drop image from old canvas
fromCanvas.remove(pendingImage);
// We're going to trick fabric.js,
// so we keep internal transforms of the source canvas,
// in order to inject it into destination canvas.
var pendingTransform = fromCanvas._currentTransform;
fromCanvas._currentTransform = null;
// Make shortcuts for fabric.util.removeListener and fabric.util.addListener
var removeListener = fabric.util.removeListener;
var addListener = fabric.util.addListener;
// Re-arrange subscriptions for source canvas
{
removeListener(fabric.document, 'mouseup', fromCanvas._onMouseUp);
removeListener(fabric.document, 'touchend', fromCanvas._onMouseUp);
removeListener(fabric.document, 'mousemove', fromCanvas._onMouseMove);
removeListener(fabric.document, 'touchmove', fromCanvas._onMouseMove);
addListener(fromCanvas.upperCanvasEl, 'mousemove', fromCanvas._onMouseMove);
addListener(fromCanvas.upperCanvasEl, 'touchmove', fromCanvas._onMouseMove, {
passive: false
});
if (isTouchDevice) {
// Wait 500ms before rebinding mousedown to prevent double triggers
// from touch devices
var _this = fromCanvas;
setTimeout(function() {
addListener(_this.upperCanvasEl, 'mousedown', _this._onMouseDown);
}, 500);
}
}
// Re-arrange subscriptions for destination canvas
{
addListener(fabric.document, 'touchend', toCanvas._onMouseUp, {
passive: false
});
addListener(fabric.document, 'touchmove', toCanvas._onMouseMove, {
passive: false
});
removeListener(toCanvas.upperCanvasEl, 'mousemove', toCanvas._onMouseMove);
removeListener(toCanvas.upperCanvasEl, 'touchmove', toCanvas._onMouseMove);
if (isTouchDevice) {
// Unbind mousedown to prevent double triggers from touch devices
removeListener(toCanvas.upperCanvasEl, 'mousedown', toCanvas._onMouseDown);
} else {
addListener(fabric.document, 'mouseup', toCanvas._onMouseUp);
addListener(fabric.document, 'mousemove', toCanvas._onMouseMove);
}
}
// We need this timer, because we want Fabric.js to complete pending render
// before we inject, because it causes some unpleasant image jumping.
setTimeout(function() {
// Add image to destination canvas,
pendingImage.scaleX *= -1;
pendingImage.canvas = toCanvas;
pendingImage.migrated = true;
toCanvas.add(pendingImage);
// and inject transforms from source canvas
toCanvas._currentTransform = pendingTransform;
// as we have mirrored the image, we mirror transforms too
toCanvas._currentTransform.scaleX *= -1;
toCanvas._currentTransform.original.scaleX *= -1;
// finally don't forget to make pasted object selected
toCanvas.setActiveObject(pendingImage);
}, 10);
};
Have fun!
Use
canvas.observe("object:moving", function (event) {});
If event.e.clientY and event.e.clientX are outside the canvas, then:
var activeObject = canvas.getActiveObject();
Store in global dragImage var
activeObject.clone(function (c) { dragImage = c; });
canvas.remove(activeObject);
Then in window mouse move event you can place an img with src = dragImage.src and follow the cursor.
function mousemove(e){
if (dragImage != null) {
$("#dragimage").show();
$("#dragimage").css("left", e.clientX);
$("#dragimage").css("top", e.clientY);
return;
}else{
$("#dragimage").hide();
}
}
On a window event mouseup, if dragImage != null and new coordinates are inside a fabric.js canvas, just newcanvas.add(dragImage).
mouseup event:
if (dragImage != null) {
$([canvas, canvas2]).each(function (i, v) {
if (Intersect([event.clientX, event.clientY],$(v.wrapperEl))) {
dragImage.left = event.clientX - $(v.wrapperEl).offset().left;
dragImage.top = event.clientY - $(v.wrapperEl).offset().top;
v.add(dragImage);
}
});
dragImage = null;
}
Helper Intersect function:
function Intersect(point, element) {
return ( point[0] > element.offset().left
&& point[0] < element.offset().left + element.width()
&& point[1] < element.offset().top + element.height()
&& point[1] > element.offset().top
);
}
css for #dragimage:
#dragimage
{
opacity:0.5;
max-width:100px;
max-height:200px;
position:fixed;
top:0px;
left:0px;
z-index:90000;
}
I can't do a fiddle but i implemented this on our mega huge photo album editor in less than 30 minutes. Works for text too but for text you must use dragImage=getActiveObject().clone()
Any questions feel free to ask.
I'm trying to make a pan-and-zoom Canvas for use as a minimap in a game. I've set the stage to be draggable so the player can move around with the mouse, as well as move individual objects on the layers of the stage. However, I don't want to be able to drag the stage into the surrounding white space. In other words, I only want to allow panning while zoomed in so you never encounter that white space. To try and constrain the stage, I've set up a dragBoundFunc:
dragBoundFunc: function(pos) {
return {
x: (pos.x < 0 ? 0 : pos.x > width ? width : pos.x),
y: (pos.y < 0 ? 0 : pos.y > height ? height : pos.y)
};
}
(Full JSFiddle example: http://jsfiddle.net/4Brry/)
I'm encountering two problems:
Firstly, the canvas is still able to move upwards and to the left.
Secondly, and more annoyingly, the constraints begin to misbehave when we begin to zoom.
When you zoom, the constraints don't take this fact into account. So, what if we add the stage offsets?
dragBoundFunc: function(pos) {
return {
x: ((ui.stage.getOffset().x+pos.x) < 0 ? 0 : pos.x > width ? width : pos.x),
y: ((ui.stage.getOffset().y+pos.y) < 0 ? 0 : pos.y > height ? height : pos.y)
};
}
(Full JSFiddle example: http://jsfiddle.net/2fLCd/)
This is a lot better, but now the view "snaps back" when you go too far. It would be nicer if it just stopped moving in the disallowed direction.
Anyone know how I could fix these issues?
Ok, I integrated the Zynga Scroller functionality with the KineticJS framework to get what I wanted.
Code in action
Let's step look at the code, which is an amalgamation of things I found online and wrote myself.
First, we generate the canvas using KineticJS:
var width = 700;
var height = 700;
var stage = new Kinetic.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Kinetic.Layer({});
stage.add(layer);
/* I skipped some circle generation code. */
Then, we define some events that fire when dragging and dropping something on the layer. We'll use these to populate a global variable called somethingIsBeingDraggedInKinetic. We'll use this variable in the panning code of Zynga Scroller so the entire stage isn't moved around when you're dragging a KineticJS shape.
var somethingIsBeingDraggedInKinetic = false;
layer.on('dragstart', function(evt) {
// get the thing that is being dragged
var thing = evt.targetNode;
if( thing )
somethingIsBeingDraggedInKinetic = true;
});
layer.on('dragend', function(evt) {
// get the thing that is being dragged
var thing = evt.targetNode;
if( thing )
somethingIsBeingDraggedInKinetic = false;
});
Next up is the Zynga Scroller initialization code. The Zynga Scroller code handles input and transformations, and then passes on three values to a rendering function: top, left and zoom. These values are perfect for passing on to the KineticJS framework:
// Canvas renderer
var render = function(left, top, zoom) {
// Constrain the stage from going too far to the right
if( (left + (width / zoom)) > width )
left = width - (width / zoom );
// Constrain the stage from going too far to the left
if( (top + (height / zoom)) > height )
top = height - (height / zoom );
stage.setOffset(left, top);
stage.setScale(zoom);
stage.draw();
};
// Initialize Scroller
this.scroller = new Scroller(render, {
zooming: true,
animating: false,
bouncing: false,
locking: false,
minZoom: 1
});
After that, we need to position the Zynga Scroller correctly. I'll admit that this part is a bit of black magic for me. I copied the rest of the code over from the "asset/ui.js" file.
var container = document.getElementById("container");
var rect = container.getBoundingClientRect();
scroller.setPosition(rect.left + container.clientLeft, rect.top + container.clientTop);
scroller.setDimensions(700, 700, width, height);
Finally, I copied over the panning code as well, and added some code that checks if the KineticJS framework is moving something:
var mousedown = false;
container.addEventListener("mousedown", function(e) {
if (e.target.tagName.match(/input|textarea|select/i)) {
return;
}
scroller.doTouchStart([{
pageX: e.pageX,
pageY: e.pageY
}], e.timeStamp);
mousedown = true;
}, false);
document.addEventListener("mousemove", function(e) {
if (somethingIsBeingDraggedInKinetic)
return;
if (!mousedown) {
return;
}
scroller.doTouchMove([{
pageX: e.pageX,
pageY: e.pageY
}], e.timeStamp);
mousedown = true;
}, false);
document.addEventListener("mouseup", function(e) {
if (!mousedown) {
return;
}
scroller.doTouchEnd(e.timeStamp);
mousedown = false;
}, false);
Oh, and the zoom handler.
container.addEventListener(navigator.userAgent.indexOf("Firefox") > -1 ? "DOMMouseScroll" : "mousewheel", function(e) {
scroller.doMouseZoom(e.detail ? (e.detail * -120) : e.wheelDelta, e.timeStamp, e.pageX, e.pageY);
}, false);
This is perfect as a basis for a zoomable map!