How do I add an object next to a moving object in Phaser? - phaser-framework

In the snippet below, I place two red squares next to each other. You can see there is no gap between them.
If I try to do the same thing with moving objects, there is a slight gap. What's the right way to do this?
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.min.js"></script>
</head>
<body>
<script>
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: { default: 'arcade' },
scene: { create: create, update: update },
};
var game = new Phaser.Game(config);
var red, red2, yellow, yellow2;
const velocity = -300;
function create() {
red = this.add.rectangle(400, 200, 100, 100, 0xff0000).setOrigin(0, 0);
red2 = this.add.rectangle(red.x + 100, 250, 100, 100, 0xff6666).setOrigin(0, 0);
yellow = this.add.rectangle(400, 400, 100, 100, 0xffff00).setOrigin(0, 0);
this.physics.add.existing(yellow);
yellow.body.velocity.x = velocity;
}
function update() {
if (yellow.x < 200 && !yellow2) {
yellow2 = this.add.rectangle(yellow.x + 100, 450, 100, 100, 0xffff66).setOrigin(0, 0);
this.physics.add.existing(yellow2);
yellow2.body.velocity.x = velocity;
}
if (yellow.x < -200) {
this.scene.restart();
yellow2 = undefined;
}
}
</script>
</body>
</html>

I think the solution is: Use the position of the physics body by changing yellow.x + 100 to yellow.body.x + 100.
(Or maybe yellow.body.position.x + 100? Not sure what the difference is, or if body.x is just an alias for body.position.x.)
Related: https://phaser.discourse.group/t/lag-in-dom-sprite-position-updates/798

A easy way out is to use the Phaser function: Phaser.Display.Align.To.RightCenter and the event: postupdate.
You then just have to calculate the offset between the objects and add pass it into the function. ( documentation to the Align function). this is based on this solution: phaser forum
canvas{
transform: translate(-40%, -40%) scale(.3);
}
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.min.js"></script>
</head>
<body style="overflow:hidden">
<script>
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: { default: 'arcade' },
scene: { create: create, update: update },
};
var game = new Phaser.Game(config);
var red, red2, yellow, yellow2;
const velocity = -300;
var yOffSet = 0;
function create() {
red = this.add.rectangle(400, 200, 100, 100, 0xff0000).setOrigin(0, 0);
red2 = this.add.rectangle(red.x + 100, 250, 100, 100, 0xff6666).setOrigin(0, 0);
yellow = this.add.rectangle(400, 400, 100, 100, 0xffff00).setOrigin(0, 0);
this.physics.add.existing(yellow);
yellow.body.velocity.x = velocity;
this.events.on("postupdate", function () {
if (yellow && yellow2) {
Phaser.Display.Align.To.RightCenter(yellow2, yellow, 0, yOffSet);
}
});
}
function update(time, delta) {
if (yellow.x < 200 && !yellow2) {
yellow2 = this.add.rectangle(yellow.x + 100, 450, 100, 100, 0xffff66).setOrigin(0, 0);
this.physics.add.existing(yellow2);
yellow2.body.velocity.x = velocity;
yOffSet = yellow2.y - yellow.y
}
if (yellow.x < -200) {
this.scene.restart();
yellow2 = undefined
}
}
</script>
</body>
</html>
Alternativly you could, calculate the x position more exact, but it is no easy since, the update function is not called in the same time interval. This could cause minor overlap or splitting.
// deltaTime being the the second paramenter of the update function
yellow2 = this.add.rectangle(yellow.x + 100 + (velocity * deltaTime), 450, 100, 100, 0xff0000).setOrigin(0, 0);
Or even easier, you could simply set the x in the postUpdate event (without Align)
this.events.on("postupdate", function () {
if ( yellow2) {
yellow2.x =yellow.x + 100;
}
});

Related

When I move camera the polygons disappear from canvas

I am making isomorphic map maker (or am trying to so far) and am moving camera using the arrow keys. The problem I have is that when I go beyond the canvas range the map is cut.
It seems as it erases the parts that are not in canvas's range? I don't understand...
When I zoom out the objects are there, but when I reset the zoom the objects are not visible again.
Here is the code fragment I am using (I removed the debug mode and it is not even showing the polygons ;( ):
game.ts:
import 'phaser';
export default class Demo extends Phaser.Scene {
private cameraMoveSpeed: number = 6;
private tileSize: number = 64; // pixels
private cursors: Phaser.Types.Input.Keyboard.CursorKeys;
constructor() {
super('demo');
}
preload() {
this.load.image('grass', 'assets/grass.png');
}
create() {
// This will create a new object called "cursors", inside it will contain 4 objects: up, down, left and right.
// These are all Phaser.Key objects, so anything you can do with a Key object you can do with these.
this.cursors = this.input.keyboard.createCursorKeys();
this.cameras.main
.setBounds(-2048, -2048, 2048 * 2, 2048 * 2, true) // TODO what should this be?
.centerOn(0, 0)
.setZoom(1);
for (let i = 0; i < 20; i++) {
for (let j = 0; j < 20; j++) {
const x = j * this.tileSize;
const y = i * this.tileSize;
placetile.call(this, x, y); // Place tiles in isometric coordinates
}
}
// Zoom keys
this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A)
.on('down', function (key, event) {
console.log('Plus Clicked');
if (this.cameras.main.zoom < 2) {
this.cameras.main.zoom += 0.25;
}
}, this);
let minusKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S)
.on('down', function (key, event) {
console.log('Minus Clicked');
if (this.cameras.main.zoom > 0.25) {
this.cameras.main.zoom -= 0.25;
}
}, this);
this.input.on('wheel', function (pointer, gameObjects, deltaX, deltaY, deltaZ) {
if (deltaY < 0) {
console.log('Scrolled up (rotate left)');
} else {
console.log('Scrolled down (rotate right)');
}
});
}
update() {
// For example this checks if the up or down keys are pressed and moves the camera accordingly.
if (this.cursors.up.isDown) {
this.cameras.main.y += this.cameraMoveSpeed;
}
else if (this.cursors.down.isDown) {
this.cameras.main.y -= this.cameraMoveSpeed;
}
if (this.cursors.left.isDown) {
this.cameras.main.x += this.cameraMoveSpeed;
}
else if (this.cursors.right.isDown) {
this.cameras.main.x -= this.cameraMoveSpeed;
}
}
}
function placetile(x, y) {
const isoPoint = cartesianToIsometric(new Phaser.Geom.Point(x, y));
console.log(isoPoint, this.tileSize);
const tile = this.add.polygon(isoPoint.x, isoPoint.y, [
this.tileSize, 0,
0, this.tileSize / 2,
0 + this.tileSize, this.tileSize,
0 + this.tileSize * 2, this.tileSize / 2
], 0xeeeeee)
// const tile = this.add.sprite(isoPoint.x, isoPoint.y, 'grass')
.setOrigin(0.5, 0.5)
// .setDepth(y)
.setInteractive(new Phaser.Geom.Polygon([
this.tileSize, 0,
0, this.tileSize / 2,
0 + this.tileSize, this.tileSize,
0 + this.tileSize * 2, this.tileSize / 2,
]), Phaser.Geom.Polygon.Contains)
.on('pointerover', function (event) {
this.setStrokeStyle(4, 0x7878ff, 0.5);
})
.on('pointerout', function (event) {
this.setStrokeStyle(0);
});
console.log(tile);
// this.input.enableDebug(tile, 0xff00ff);
}
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
backgroundColor: '#eeeeee',
scale: {
width: 1280,
height: 1024,
parent: 'content'
// mode: Phaser.Scale.RESIZE ????
},
scene: Demo,
// physics: {
// default: 'arcade',
// arcade: {
// debug: true
// }
// },
};
const game = new Phaser.Game(config);
function cartesianToIsometric(cartPt) {
return new Phaser.Geom.Point(
cartPt.x - cartPt.y,
(cartPt.x + cartPt.y) / 2
);
}
function isometricToCartesian(isoPt) {
var tempPt = new Phaser.Geom.Point();
tempPt.x = (2 * isoPt.y + isoPt.x) / 2;
tempPt.y = (2 * isoPt.y - isoPt.x) / 2;
return (tempPt);
}
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phaser Demo</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<style>
body {
margin: 0;
padding: 0;
}
canvas {
width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="container">
<div id="content"></div>
</div>
<script src="game.js"></script>
</body>
You're using this.camera.main.x and this.camera.main.y. Those properties seem as though they don't update rendering for previously offscreen textures. I linked a copy of your code that works when using this.camera.main.scrollX and this.camera.main.scrollY. Those are the correct properties to use for scrolling the camera.
Here's a link to the docs that explicitly states scrollX/scrollY as the properties to use for scrolling:
https://photonstorm.github.io/phaser3-docs/Phaser.Cameras.Scene2D.Camera.html#scrollX__anchor
Copied your code here with those changes to show working:
https://stackblitz.com/edit/phaser-camera-main-scrollx-not-x

Fabricjs - selection only via border

I'm using Fabric.js to draw some rectangles on a canvas. The default behavior is that clicking inside a rectangle selects it. How can I change the behavior such that it is only selected when clicking on the border of the rectangle?
Clicking inside the rectangle but not on the border should do nothing.
You can see this behavior by drawing a rectangle on a TradingView.com chart
It there an option for this in fabric, and if not how could I go around implementing it?
This approach overrides the _checkTarget method within FabricJS to reject clicks that are more than a specified distance from the border (defined by the clickableMargin variable).
//sets the width of clickable area
var clickableMargin = 15;
var canvas = new fabric.Canvas("canvas");
canvas.add(new fabric.Rect({
width: 150,
height: 150,
left: 25,
top: 25,
fill: 'green',
strokeWidth: 0
}));
//overrides the _checkTarget method to add check if point is close to the border
fabric.Canvas.prototype._checkTarget = function(pointer, obj, globalPointer) {
if (obj &&
obj.visible &&
obj.evented &&
this.containsPoint(null, obj, pointer)){
if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) {
var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y);
if (!isTransparent) {
return true;
}
}
else {
var isInsideBorder = this.isInsideBorder(obj);
if(!isInsideBorder) {
return true;
}
}
}
}
fabric.Canvas.prototype.isInsideBorder = function(target) {
var pointerCoords = target.getLocalPointer();
if(pointerCoords.x > clickableMargin &&
pointerCoords.x < target.getScaledWidth() - clickableMargin &&
pointerCoords.y > clickableMargin &&
pointerCoords.y < target.getScaledHeight() - clickableMargin) {
return true;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="canvas" height="300" width="400"></canvas>
Fabric.js uses Object.containsPoint() to determine whether a mouse event should target the object. This method, in turn, calculates the object's edges via Object._getImageLines() and checks how many times the projection of a mouse pointer crossed those lines.
The solution below calculates additional inner edges based on the coordinates of each corner, therefore object scale and rotation are taken care of automatically.
const canvas = new fabric.Canvas('c', {
enableRetinaScaling: true
})
const rect = new fabric.Rect({
left: 0,
top: 0,
width: 100,
height: 100,
dragBorderWidth: 15, // this is the custom attribute we've introduced
})
function innerCornerPoint(start, end, offset) {
// vector length
const l = start.distanceFrom(end)
// unit vector
const uv = new fabric.Point((end.x - start.x) / l, (end.y - start.y) / l)
// point on the vector at a given offset but no further than side length
const p = start.add(uv.multiply(Math.min(offset, l)))
// rotate point
return fabric.util.rotatePoint(p, start, fabric.util.degreesToRadians(45))
}
rect._getInnerBorderLines = function(c) {
// the actual offset from outer corner is the length of a hypotenuse of a right triangle with border widths as 2 sides
const offset = Math.sqrt(2 * (this.dragBorderWidth ** 2))
// find 4 inner corners as offsets rotated 45 degrees CW
const newCoords = {
tl: innerCornerPoint(c.tl, c.tr, offset),
tr: innerCornerPoint(c.tr, c.br, offset),
br: innerCornerPoint(c.br, c.bl, offset),
bl: innerCornerPoint(c.bl, c.tl, offset),
}
return this._getImageLines(newCoords)
}
rect.containsPoint = function(point, lines, absolute, calculate) {
const coords = calculate ? this.calcCoords(absolute) : absolute ? this.aCoords : this.oCoords
lines = lines || this._getImageLines(coords)
const innerRectPoints = this._findCrossPoints(point, lines);
const innerBorderPoints = this._findCrossPoints(point, this._getInnerBorderLines(coords))
// calculate intersections
return innerRectPoints === 1 && innerBorderPoints !== 1
}
canvas.add(rect)
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="c" width="400" height="300"></canvas>
here is my approach, when rect is clicked I am calculating where it is clicked and
if it is not clicked on border I have to set canvas.discardActiveObject , see comments on code
var canvas = new fabric.Canvas('c', {
selection: false
});
var rect = new fabric.Rect({
left: 50,
top: 50,
width: 100,
height: 100,
strokeWidth: 10,
stroke: 'red',
selectable: false,
evented: true,
hasBorders: true,
lockMovementY: true,
lockMovementX: true
})
canvas.on("mouse:move", function(e) {
if (!e.target || e.target.type != 'rect') return;
// when selected event is fired get the click position.
var pointer = canvas.getPointer(e.e);
// calculate the click distance from object to be exact
var distanceX = pointer.x - rect.left;
var distanceY = pointer.y - rect.top;
// check if click distanceX/Y are less than 10 (strokeWidth) or greater than 90 ( rect width = 100)
if ((distanceX <= rect.strokeWidth || distanceX >= (rect.width - rect.strokeWidth)) || (distanceY <= rect.strokeWidth || distanceY >= (rect.height - rect.strokeWidth))) {
rect.set({
hoverCursor: 'move',
selectable: true,
lockMovementY: false,
lockMovementX: false
});
document.getElementById('result').innerHTML = 'on border';
} else {
canvas.discardActiveObject();
document.getElementById('result').innerHTML = 'not on border';
rect.set({
hoverCursor: 'default',
selectable: false,
lockMovementY: true,
lockMovementX: true
});
}
});
canvas.add(rect);
canvas.renderAll();
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<div id="result" style="width: 100%; "></div>
<canvas id="c" width="600" height="200"></canvas>
<pre>
</pre>
ps: you can also set the rect property to selectable: false and call canvas.setActiveObject(this); to make it selection inside if statement.

Proper way to approach loadable masks in Fabric.js

I have this bounty open Fabricjs mask object with transformation when trying to mask objects with Fabric.js.
The tool I'm developing should allow users to draw a mask over image objects, and apply transformations (skew scale rotate etc) to this object before or after the mask. I'm close to obtaining this result but objects with an angle are still not working.
I'm also trying to save this object to a database using toJSON and loadFromJSON, but after a few days trying to accomplish this I realize that this solution will not work because any references outside the ctx scope can't be accessed while loading from JSON, so they throw an error.
clipTo: function(ctx) {
mask.set({
left:
-object.width / 2 -
(mask.width / 2) * originalMaskScaleX -
originalObjLeft / originalObjScaleX,
top:
-object.height / 2 -
(mask.height / 2) * originalMaskScaleY -
originalObjTop / originalObjScaleY,
objectCaching: false
});
mask.render(ctx);
}
Is Fabric.js the proper solution to this problem? Should I be using something else? If this can be done with Fabric.js, what is the proper approach?
I extended fabric.Image with some custom attributes.
Also I attached the mask on fabric.Image.
For fabric.Image.fromObject after the image is loaded I need it to load also the mask( which I know is a path) and attach to image.
This is a fast implementation. I'm pretty sure this code can be simplified.
Please tell me know if something is not clear enougth
canvas = new fabric.Canvas("canvas", {
backgroundColor: "lightgray",
width: 1280,
height: 720,
preserveObjectStacking: true,
selection: false,
stateful: true
});
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = "black";
canvas.freeDrawingBrush.width = 2;
canvas.on("path:created", function(options) {
clip(options.path);
});
function clip(path) {
canvas.isDrawingMode = false;
canvas.remove(path);
let mask = new fabric.Path(path.path, {
top: object.top,
left: object.left,
objectCaching: false,
strokeWidth: 0,
scaleX: 1 / object.scaleX,
scaleY: 1 / object.scaleY,
pathOffset: {
x: 0,
y: 0
}
});
object = canvas.getObjects()[0];
object.originalObjLeft = object.left,
object.originalObjTop = object.top,
object.originalMaskScaleX = mask.scaleX,
object.originalMaskScaleY = mask.scaleY,
object.originalObjScaleX = object.scaleX,
object.originalObjScaleY = object.scaleY;
var transformedTranslate = object.translateToGivenOrigin({
x: object.left,
y: object.top
}, object.originX, object.originY, 'center', 'center');
object.originalTransformLeft = transformedTranslate.x - object.getCenterPoint().x;
object.originalTransformTop = transformedTranslate.y - object.getCenterPoint().y;
object.originalAngle = object.angle;
object.clipMask = mask;
object.set({
clipTo: function(ctx) {
ctx.save();
ctx.rotate(-this.originalAngle * Math.PI / 180);
ctx.translate(this.originalTransformLeft / this.originalObjScaleX, this.originalTransformTop / this.originalObjScaleY)
this.clipMask.set({
left: -object.width / 2 - (this.clipMask.width / 2 * this.originalMaskScaleX) - this.originalObjLeft / this.originalObjScaleX,
top: -object.height / 2 - (this.clipMask.height / 2 * this.originalMaskScaleY) - this.originalObjTop / this.originalObjScaleY,
objectCaching: false
});
this.clipMask.render(ctx);
ctx.restore();
}
});
canvas.requestRenderAll();
}
// image
let image = new Image();
image.onload = function() {
object = new fabric.Image(image, {
width: 500,
height: 500,
scaleX: 0.8,
scaleY: 0.8,
angle: 45,
top: 50,
left: 100
});
canvas.add(object);
};
image.src = "http://i.imgur.com/8rmMZI3.jpg";
fabric.util.object.extend(fabric.Image.prototype, {
clipMask: null,
originalObjLeft: 0,
originalObjTop: 0,
originalMaskScaleX: 1,
originalMaskScaleY: 1,
originalObjScaleX: 1,
originalObjScaleY: 1,
originalAngle:0,
originalTransformLeft:0,
originalTransformTop:0
});
fabric.Image.prototype.toObject = (function(toObject) {
return function(propertiesToInclude) {
return fabric.util.object.extend(toObject.call(this, propertiesToInclude), {
clipMask: this.clipMask ? this.clipMask.toObject(propertiesToInclude) : null,
originalObjLeft: this.originalObjLeft,
originalObjTop: this.originalObjTop,
originalMaskScaleX: this.originalMaskScaleX,
originalMaskScaleY: this.originalMaskScaleY,
originalObjScaleX: this.originalObjScaleX,
originalObjScaleY: this.originalObjScaleY,
originalAngle:this.originalAngle,
originalTransformLeft:this.originalTransformLeft,
originalTransformTop:this.originalTransformTop
});
}
})(fabric.Image.prototype.toObject);
fabric.Image.fromObject = (function(fromObject) {
return function(_object, callback) {
fromObject.call(this, _object, (function(callback, _object) {
return function(image) {
if (image.clipMask) {
fabric.Path.fromObject(image.clipMask, (function(callback) {
return function(path) {
path.pathOffset.x = 0;
path.pathOffset.y = 0;
image.clipMask = path;
callback(image);
}
})(callback))
} else {
callback(image);
}
}
})(callback, _object));
return;
}
})(fabric.Image.fromObject)
$("#button1").on('click', function() {
let dataJSON = canvas.toJSON();
canvas.clear();
canvas.loadFromJSON(
dataJSON,
canvas.renderAll.bind(canvas));
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.6/fabric.js"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<button id="button1">SAve/Load JSON</button>
<div class="canvas__wrapper">
<canvas id="canvas" width="1280" height="720"></canvas>
</div>
UPDATE
I updated the code to fix the problem with angle from here:

how to render an object reappeared on the canvas

Initially I instantiated a Rect object, by controlling the object's top and left values, making it beyond the canvas area, so that the Rect object will not be rendered on the canvas. After that, change the top and left values of the Rect to make it in the area of the canvas by the event handler and then how to render the Rect object on the canvas.
the following code is a demo:
<canvas id="canvas" width="800" height="600"></canvas>
<script src="js/fabric.js"></script>
<script>
(function () {
var canvas = this.__canvas = new fabric.Canvas('canvas');
fabric.Object.prototype.transparentCorners = false;
var targetLine = [], paramsG, paramsR;
for (var k = 0; k < 20; k++) {
paramsG = {
left: 200,
top: 530 - 100 * k,
width: 20,
height: 50,
visibile: false,
fill: '#62ab59',
hasBorders: false,
lockMovementX: true,
hasControls: false
};
paramsR = {
left: 200,
top: 580 - 100 * k,
width: 20,
height: 50,
visibile: false,
fill: '#ed5d5d',
hasBorders: false,
lockMovementX: true,
hasControls: false
};
canvas.add(new fabric.Rect(paramsG), new fabric.Rect(paramsR));
}
canvas.on('mouse:down', function (e) {
if (e.target) {
targetLine = getMemberByLeft(canvas._objects, e.target);
}
})
canvas.on('object:moving', function (e) {
targetLine.forEach(function (val) {
canvas._objects[val.index].set({top: e.e.movementY + canvas._objects[val.index].top});
})
canvas.renderAll();
})
function getMemberByLeft(arr, tar) {
var returnArr = [];
arr.forEach(function (value, key) {
if (value.left == tar.left && value != tar) {
returnArr.push({data: value, index: key});
}
})
return returnArr;
}
})();
</script>
Fabric has a function to skip object rendering if they are not visible on screen, to get some more speed.
If you change top and left by code, fabric will not understand that the object is again on screen unless you call object.setCoords()
If you do not want to have this behaviour automatic you can disable it using
canvas.skipOffscreen = false;

Is there anyway to make canvas object auto snap align on Fabric.js [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
Im working on a project of building a editor, i am using the Fabric.js, but i don know how to make the canvas object auto align.
Is there any example of making canvas object auto align with another object like this?
image of snap example
auto snap example from other site
link here:
auto snap example site
I was working on the same thing. This seems to be a pretty common feature request so I thought I'd share what I worked out. It could use some refinement. For instance, I've noticed if you scale the red box, the scale is not applied and it doesn't align right. However, I think it demonstrates the basic principal well and you can elaborate on it to fit your needs.
(Note: 8/1/2017: Working to place a more comprehensive code base on GitHub. (https://github.com/JerrodV/FabricObjectAlignment) More Details Soon!)
You can view the fiddle [here][1]
Html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<canvas id="c" height="600" width="600" style="border:1px solid #c1c1c1;"></canvas>
</div>
</form>
<script src="Scripts/jquery-3.1.1.min.js"></script>
<script src="Scripts/fabric.min.js"></script>
<script src="Scripts/default.js"></script>
</body>
</html>
Javascript:
Def = {
canvas: null,
rect: null,
lines: {
top: null,
left: null,
right: null,
bottom: null
},
init: function () {
Def.canvas = new fabric.Canvas('c');
Def.canvas.on('object:moving', Def.events.objectMoving);
Def.canvas.add(new fabric.Rect({
height: 100,
width: 100,
top: 100,
left: 200,
fill: 'black',
selectable: false
}));
Def.canvas.add(new fabric.Rect({
height: 100,
width: 100,
top: 300,
left: 100,
fill: 'black',
selectable: false
}));
Def.rect = new fabric.Rect({
height: 100,
width: 100,
top: 200,
left: 250,
fill: 'red'
});
Def.canvas.add(Def.rect);
},
events: {
objectMoving: function (e) {
//Get the object being manipulated
var obj = e.target;
//Set up an object representing its current position
var curPos = {
top: parseInt(obj.get('top')),
left: parseInt(obj.get('left')),
right: parseInt(obj.get('left') + obj.get('width')),
bottom: parseInt(obj.get('top') + obj.get('height'))
};
//Set up an object that will let us be able to keep track of newly created lines
var matches = {
top: false,
left: false,
right: false,
bottom: false
}
//Get the objects from the canvas
var objects = Def.canvas.getObjects();
//For each object
for (var i in objects) {
//If the object we are looing at is a line or the object being manipulated, skip it
if (objects[i] === obj || objects[i].get('type') === 'line') { continue; }
//Set up an object representing the position of the canvas object
var objPos = {
top: parseInt(objects[i].get('top')),
left: parseInt(objects[i].get('left')),
right: parseInt(objects[i].get('left') + obj.get('width')),
bottom: parseInt(objects[i].get('top') + obj.get('height'))
}
//Look at all 4 sides of the object and see if the object being manipulated aligns with that side.
//Top////////////////////////////////////
if (Def.inRange(objPos.top, curPos.top)) {
//We match. If we don't already have aline on that side, add one.
if (!Def.lines.top) {
Def.drawLine('top', objPos.top);
//Keep track of the fact we found a match so we don't remove the line prematurely.
matches.top = true;
//Snap the object to the line
obj.set('top', objPos.top).setCoords();
}
}
//Left////////////////////////////////////
if (Def.inRange(objPos.left, curPos.left)) {
if (!Def.lines.left) {
Def.drawLine('left', objPos.left);
matches.left = true;
obj.set('left', objPos.left).setCoords();
}
}
//Right////////////////////////////////////
if (Def.inRange(objPos.right, curPos.right)) {
if (!Def.lines.right) {
Def.drawLine('right', objPos.right);
matches.right = true;
obj.set('left', objPos.right - objects[i].get('width')).setCoords();
}
}
//Bottom////////////////////////////////////
if (Def.inRange(objPos.bottom, curPos.bottom)) {
if (!Def.lines.bottom) {
Def.drawLine('bottom', objPos.bottom);
matches.bottom = true;
obj.set('top', objPos.bottom - objects[i].get('height')).setCoords();
}
}
//Look at the side we matched on. If we did not match, and we have a line, remove the line.
for (var j in matches) {
var m = matches[j];
var line = Def.lines[j];
if (!m && line) {
Def.canvas.remove(line);
Def.lines[j] = null;
}
}
}
Def.canvas.renderAll();
}
},
drawLine: function (side, pos) {
var ln = null
switch (side) {
case 'top':
ln = new fabric.Line([Def.canvas.get('width'), 0, 0, 0], {
left: 0,
top: pos,
stroke: 'rgb(178, 207, 255)'
});
Def.lines.top = ln;
break;
case 'left':
ln = new fabric.Line([0, Def.canvas.get('height'), 0, 0], {
left: pos,
top: 0,
stroke: 'rgb(178, 207, 255)'
});
Def.lines.left = ln;
break;
case 'right':
ln = new fabric.Line([0, Def.canvas.get('height'), 0, 0], {
left: pos,
top: 0,
stroke: 'rgb(178, 207, 255)'
});
Def.lines.right = ln;
break;
case 'bottom':
ln = new fabric.Line([Def.canvas.get('width'), 0, 0, 0], {
left: 0,
top: pos,
stroke: 'rgb(178, 207, 255)'
});
Def.lines.bottom = ln;
break;
}
Def.canvas.add(ln).renderAll();
},
alignTolerance : 6,
inRange: function (val1, val2) {
if ((Math.max(val1, val2) - Math.min(val1, val2)) <= Def.alignTolerance) { return true; }
else { return false; }
}
};
$(Def.init);
I hope you find this useful. Good Luck!
[1]: https://jsfiddle.net/cLy86rtd/

Resources