Snap edges of objects to each other and prevent overlap - javascript
My goal is to prevent overlapping of two or more rectangles inside my FabricJS canvas.
Imagine two rectangles having info on position and size and you can drag and drop any rectangle inside the canvas.
If rectangle A gets close enough to rectangle B, the position of rectangle A should snap to the edge of rectangle B. This should work for any edge of rectangle B. The vertices do not have to match, cause the sizes of the rectangles are variable.
I have a working example for this snapping on one dimension (x-axes).
My best jsfiddle attempt
See jsfiddle.
But I need it to work around the rectangle on both dimensions. I am quite sure, that my code is not well enough to manage this.
Code-snippets which might help:
object.oCoords.tl.x //top-left corner x position. similar goes for top-right (tr), bottom-left (bl), bottom-right (br) and .y for y-position
mouse_pos = canvas.getPointer(e.e);
mouse_pos.x //pointer x.position
mouse_pos.y //pointer y.position
object.intersectsWithObject(targ) // object = dragged rectangle, targ = targeted rectangle
The snapping should work for an unlimited amount of objects (not only for two rectangles).
I solved the problem on my own.
See jsfiddle: http://jsfiddle.net/gcollect/FD53A/
This is the code:
this.canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords(); //Sets corner position coordinates based on current angle, width and height
canvas.forEachObject(function (targ) {
var objects = this.canvas.getObjects(),
i = objects.length;
activeObject = canvas.getActiveObject();
if (targ === activeObject) return;
if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
activeObject.left = targ.left - activeObject.currentWidth;
}
if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
activeObject.left = targ.left + targ.currentWidth;
}
if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top - activeObject.currentHeight;
}
if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top + targ.currentHeight;
}
if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
targ.strokeWidth = 10;
targ.stroke = 'red';
} else {
targ.strokeWidth = 0;
targ.stroke = false;
}
if (!activeObject.intersectsWithObject(targ)) {
activeObject.strokeWidth = 0;
activeObject.stroke = false;
}
});
Works pretty legit! Cheers!
This is based on gco's answer, updated to work with FabricJS 1.5.0, with the following improvements:
Shapes don't overlap.
Snapping is more responsive.
Shapes are contained within the canvas.
JS Fiddle: https://jsfiddle.net/aphillips8/31qbr0vn/1/
var canvas = new fabric.Canvas('canvas'),
canvasWidth = document.getElementById('canvas').width,
canvasHeight = document.getElementById('canvas').height,
counter = 0,
rectLeft = 0,
snap = 20; //Pixels to snap
canvas.selection = false;
plusrect();
plusrect();
plusrect();
function plusrect(top, left, width, height, fill) {
var rect = new fabric.Rect({
top: 300,
name: 'rectangle ' + counter,
left: 0 + rectLeft,
width: 100,
height: 100,
fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
lockRotation: true,
originX: 'left',
originY: 'top',
cornerSize: 15,
hasRotatingPoint: false,
perPixelTargetFind: true,
minScaleLimit: 1,
maxWidth: canvasWidth,
maxHeight: canvasHeight
});
rect.custom = {};
rect.custom.counter = counter;
canvas.add(rect);
counter++;
rectLeft += 200;
}
function findNewPos(distX, distY, target, obj) {
// See whether to focus on X or Y axis
if(Math.abs(distX) > Math.abs(distY)) {
if (distX > 0) {
target.setLeft(obj.getLeft() - target.getWidth());
} else {
target.setLeft(obj.getLeft() + obj.getWidth());
}
} else {
if (distY > 0) {
target.setTop(obj.getTop() - target.getHeight());
} else {
target.setTop(obj.getTop() + obj.getHeight());
}
}
}
canvas.on('object:moving', function (options) {
// Sets corner position coordinates based on current angle, width and height
options.target.setCoords();
// Don't allow objects off the canvas
if(options.target.getLeft() < snap) {
options.target.setLeft(0);
}
if(options.target.getTop() < snap) {
options.target.setTop(0);
}
if((options.target.getWidth() + options.target.getLeft()) > (canvasWidth - snap)) {
options.target.setLeft(canvasWidth - options.target.getWidth());
}
if((options.target.getHeight() + options.target.getTop()) > (canvasHeight - snap)) {
options.target.setTop(canvasHeight - options.target.getHeight());
}
// Loop through objects
canvas.forEachObject(function (obj) {
if (obj === options.target) return;
// If objects intersect
if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {
var distX = ((obj.getLeft() + obj.getWidth()) / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
var distY = ((obj.getTop() + obj.getHeight()) / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);
// Set new position
findNewPos(distX, distY, options.target, obj);
}
// Snap objects to each other horizontally
// If bottom points are on same Y axis
if(Math.abs((options.target.getTop() + options.target.getHeight()) - (obj.getTop() + obj.getHeight())) < snap) {
// Snap target BL to object BR
if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth());
options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
}
// Snap target BR to object BL
if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
options.target.setLeft(obj.getLeft() - options.target.getWidth());
options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
}
}
// If top points are on same Y axis
if(Math.abs(options.target.getTop() - obj.getTop()) < snap) {
// Snap target TL to object TR
if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth());
options.target.setTop(obj.getTop());
}
// Snap target TR to object TL
if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
options.target.setLeft(obj.getLeft() - options.target.getWidth());
options.target.setTop(obj.getTop());
}
}
// Snap objects to each other vertically
// If right points are on same X axis
if(Math.abs((options.target.getLeft() + options.target.getWidth()) - (obj.getLeft() + obj.getWidth())) < snap) {
// Snap target TR to object BR
if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
options.target.setTop(obj.getTop() + obj.getHeight());
}
// Snap target BR to object TR
if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
options.target.setTop(obj.getTop() - options.target.getHeight());
}
}
// If left points are on same X axis
if(Math.abs(options.target.getLeft() - obj.getLeft()) < snap) {
// Snap target TL to object BL
if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
options.target.setLeft(obj.getLeft());
options.target.setTop(obj.getTop() + obj.getHeight());
}
// Snap target BL to object TL
if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
options.target.setLeft(obj.getLeft());
options.target.setTop(obj.getTop() - options.target.getHeight());
}
}
});
options.target.setCoords();
// If objects still overlap
var outerAreaLeft = null,
outerAreaTop = null,
outerAreaRight = null,
outerAreaBottom = null;
canvas.forEachObject(function (obj) {
if (obj === options.target) return;
if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {
var intersectLeft = null,
intersectTop = null,
intersectWidth = null,
intersectHeight = null,
intersectSize = null,
targetLeft = options.target.getLeft(),
targetRight = targetLeft + options.target.getWidth(),
targetTop = options.target.getTop(),
targetBottom = targetTop + options.target.getHeight(),
objectLeft = obj.getLeft(),
objectRight = objectLeft + obj.getWidth(),
objectTop = obj.getTop(),
objectBottom = objectTop + obj.getHeight();
// Find intersect information for X axis
if(targetLeft >= objectLeft && targetLeft <= objectRight) {
intersectLeft = targetLeft;
intersectWidth = obj.getWidth() - (intersectLeft - objectLeft);
} else if(objectLeft >= targetLeft && objectLeft <= targetRight) {
intersectLeft = objectLeft;
intersectWidth = options.target.getWidth() - (intersectLeft - targetLeft);
}
// Find intersect information for Y axis
if(targetTop >= objectTop && targetTop <= objectBottom) {
intersectTop = targetTop;
intersectHeight = obj.getHeight() - (intersectTop - objectTop);
} else if(objectTop >= targetTop && objectTop <= targetBottom) {
intersectTop = objectTop;
intersectHeight = options.target.getHeight() - (intersectTop - targetTop);
}
// Find intersect size (this will be 0 if objects are touching but not overlapping)
if(intersectWidth > 0 && intersectHeight > 0) {
intersectSize = intersectWidth * intersectHeight;
}
// Set outer snapping area
if(obj.getLeft() < outerAreaLeft || outerAreaLeft == null) {
outerAreaLeft = obj.getLeft();
}
if(obj.getTop() < outerAreaTop || outerAreaTop == null) {
outerAreaTop = obj.getTop();
}
if((obj.getLeft() + obj.getWidth()) > outerAreaRight || outerAreaRight == null) {
outerAreaRight = obj.getLeft() + obj.getWidth();
}
if((obj.getTop() + obj.getHeight()) > outerAreaBottom || outerAreaBottom == null) {
outerAreaBottom = obj.getTop() + obj.getHeight();
}
// If objects are intersecting, reposition outside all shapes which touch
if(intersectSize) {
var distX = (outerAreaRight / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
var distY = (outerAreaBottom / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);
// Set new position
findNewPos(distX, distY, options.target, obj);
}
}
});
});
I based this fiddle off #Anna Phillips' and #gco's examples. It includes:
Corner snapping
Edge snapping
Objects can overlap
Objects are fully contained within the canvas
Objects cannot have a size larger than the canvas area
Here is the code:
window.canvas = new fabric.Canvas('fabriccanvas');
window.counter = 0;
var newleft = 0,
edgedetection = 20, //pixels to snap
canvasWidth = document.getElementById('fabriccanvas').width,
canvasHeight = document.getElementById('fabriccanvas').height;
canvas.selection = false;
plusrect();
plusrect();
plusrect();
function plusrect(top, left, width, height, fill) {
window.canvas.add(new fabric.Rect({
top: 300,
name: 'rectangle ' + window.counter,
left: 0 + newleft,
width: 100,
height: 100,
fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
lockRotation: true,
originX: 'left',
originY: 'top',
cornerSize: 15,
hasRotatingPoint: false,
perPixelTargetFind: true,
minScaleLimit: 1,
maxHeight: document.getElementById("fabriccanvas").height,
maxWidth: document.getElementById("fabriccanvas").width,
}));
window.counter++;
newleft += 200;
}
this.canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords(); //Sets corner position coordinates based on current angle, width and height
if(obj.getLeft() < edgedetection) {
obj.setLeft(0);
}
if(obj.getTop() < edgedetection) {
obj.setTop(0);
}
if((obj.getWidth() + obj.getLeft()) > (canvasWidth - edgedetection)) {
obj.setLeft(canvasWidth - obj.getWidth());
}
if((obj.getHeight() + obj.getTop()) > (canvasHeight - edgedetection)) {
obj.setTop(canvasHeight - obj.getHeight());
}
canvas.forEachObject(function (targ) {
activeObject = canvas.getActiveObject();
if (targ === activeObject) return;
if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
activeObject.left = targ.left - activeObject.currentWidth;
}
if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
activeObject.left = targ.left + targ.currentWidth;
}
if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top - activeObject.currentHeight;
}
if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
activeObject.top = targ.top + targ.currentHeight;
}
if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
targ.strokeWidth = 10;
targ.stroke = 'red';
} else {
targ.strokeWidth = 0;
targ.stroke = false;
}
if (!activeObject.intersectsWithObject(targ)) {
activeObject.strokeWidth = 0;
activeObject.stroke = false;
}
});
});
What I'd like to know is if it's possible to extend this to add the following features:
Dynamic snapping. Continuing to drag an object after the initial snap will temporarily disable snapping until the object stops moving. For example, if I drag one box next to another, they will snap together once they are within range. However if I continue moving the first box, I can "drop" it in a position where it is within the snapping range but not aligned to the other box.
Show guide lines when selected object is within range of another object. Currently we add a border around the target object, but it would be better to show guidelines that extend outwards (possibly to the edge of the canvas) to more easily visualize the bounds of the target object.
Parallel snapping. When moving an object that is already snapped to the target object, the selected object should snap to the target object in such a way that the tops, bottoms, or sides of both objects are parallel. For example, assume that the selected square is snapped to the left of the target square and that the top of the selected square is below the top of the target square. Moving the selected square up should cause its top to snap into alignment with the top of the target once in range. The same logic should apply when moving it down, or if the selected object is above/below the target and being moves horizontally.
I needed snapping of unequal sized areas. jsfiddle
var canvas = new fabric.Canvas('c');
canvas.setDimensions({width:window.innerWidth});
var edge_detection_external = 21;
var corner_detection = 5;
canvas.selection = false;
canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords();
function update_position(obj){
return function(targ){
if(targ === obj) return;
// Check overlap case https://www.geeksforgeeks.org/find-two-rectangles-overlap/
if(!(function(targ,obj){
if(obj.aCoords.tl.x > targ.aCoords.br.x || targ.aCoords.tl.x > obj.aCoords.br.x)
return false;
if(targ.aCoords.tl.y > obj.aCoords.br.y || obj.aCoords.tl.y > targ.aCoords.br.y)
return false;
return true;
})(targ,obj)){
// is on RIGHT or LEFT?
if((obj.top > targ.top && obj.top < targ.top + targ.height)
|| (targ.top > obj.top && targ.top < obj.top + obj.height)){
// Object is to the RIGHT and Edge detection
if(obj.aCoords.tl.x > targ.aCoords.br.x
&& obj.aCoords.tl.x - targ.aCoords.br.x < edge_detection_external){
obj.set({left:targ.aCoords.br.x});
// Corner detection
obj.setCoords();
if(Math.abs(targ.aCoords.tr.y - obj.aCoords.tl.y) < corner_detection)
obj.set({top:targ.top});
else if(Math.abs(targ.aCoords.br.y - obj.aCoords.bl.y) < corner_detection)
obj.set({top:targ.top + targ.height - obj.height});
}
// LEFT
if(targ.aCoords.tl.x > obj.aCoords.br.x
&& targ.aCoords.tl.x - obj.aCoords.br.x < edge_detection_external){
obj.set({left:targ.aCoords.tl.x - obj.width});
obj.setCoords();
if(Math.abs(targ.aCoords.tl.y - obj.aCoords.tr.y) < corner_detection)
obj.set({top:targ.top});
else if(Math.abs(targ.aCoords.bl.y - obj.aCoords.br.y) < corner_detection)
obj.set({top:targ.top + targ.height - obj.height});
}
}
// is on TOP or BOTTOM?
if((obj.left > targ.left && obj.left < targ.left + targ.width)
|| (targ.left > obj.left && targ.left < obj.left + obj.width)){
// TOP
if(targ.aCoords.tl.y > obj.aCoords.br.y
&& targ.aCoords.tl.y - obj.aCoords.br.y < edge_detection_external){
obj.set({top:targ.aCoords.tl.y - obj.height});
obj.setCoords();
if(Math.abs(targ.aCoords.tl.x - obj.aCoords.bl.x) < corner_detection)
obj.set({left:targ.left});
else if(Math.abs(targ.aCoords.tr.x - obj.aCoords.br.x) < corner_detection)
obj.set({left:targ.left + targ.width - obj.width});
}
// BOTTOM
if(obj.aCoords.tl.y > targ.aCoords.br.y
&& obj.aCoords.tl.y - targ.aCoords.br.y < edge_detection_external){
obj.set({top:targ.aCoords.br.y});
obj.setCoords();
if(Math.abs(targ.aCoords.bl.x - obj.aCoords.tl.x) < corner_detection)
obj.set({left:targ.left});
else if(Math.abs(targ.aCoords.br.x - obj.aCoords.tr.x) < corner_detection)
obj.set({left:targ.left + targ.width - obj.width});
}
}
}
}
}
canvas.getObjects('group').some(update_position(obj));
});
String.prototype.to_inches = function(){
return this.split('-').map(function(value,index){
value = Number(value);
if(index == 0)
return value * 12
else
return value
}).reduce(function(total,current){
return total + current;
});
}
Array.prototype.to_object_list = function(){
const preserved = [...this];
var header = this.splice(0,1)[0];
for(var i = 0;i < this.length; i++){
var obj = {};
for(var j = 0;j < header.length; j++){
obj[header[j].toLowerCase()] = this[i][j];
}
this[i] = obj;
}
return preserved;
}
function draw_areas(){
var offset = 0;
return function(area_params,index){
if(area_params.area.indexOf('>') === -1){
var area = new fabric.Rect({
fill: 'red',
width:area_params.width.to_inches(),
height:area_params.length.to_inches(),
});
var text = new fabric.Text(area_params.area + '\n' + area_params.width + ' x ' + area_params.length,{
fontSize:12,
fill:"white"
});
if(text.width - area.width > 0){
text.set('width',area.width);
}
if(text.height - area.height > 0){
text.set('height',area.height);
}
var group_name = 'group_' + area_params.area.split(' ').join('-');
var group = new fabric.Group([area,text],{
name: group_name,
left: 5,
top: 5*(index++) + offset,
});
canvas.add(group);
offset = area_params.length.to_inches() + offset;
canvas.setDimensions({height:5*(index++) + offset});
}
}
}
function handler_get_data(data){
data = JSON.parse(data);
data.to_object_list();
data.forEach(draw_areas());
}
var d = '[["Area","Width","Length"],["Bedroom 1","19-5.5","14"],["Kitchen","14","16-3"],["Bedroom 2","13-6","12-9"]]';
handler_get_data(d);
Related
JS Canvas movement animation loop
I have an object rendered to a canvas. I'm trying to get the object to move along a set path on a loop. Here is what I have: // Canvas Element var canvas = null; // Canvas Draw var ctx = null; // Static Globals var tileSize = 16, mapW = 10, mapH = 10; // Instances of entities var entities = [ // A single entity that starts at tile 28, and uses the setPath() function { id: 0, tile: 28, xy: tileToCoords(28), width: 16, height: 24, speedX: 0, speedY: 0, logic: { func: 'setPath', // These are the parameters that go into the setPath() function data: [0, ['down', 'up', 'left', 'right'], tileToCoords(28), 0] }, dir: {up:false, down:false, left:false, right:false} } ]; // Array for tile data var map = []; window.onload = function(){ // Populate the map array with a blank map and 4 walls testMap(); canvas = document.getElementById('save'); ctx = canvas.getContext("2d"); // Add all the entities to the map array and start their behavior for(var i = 0; i < entities.length; ++i){ map[entities[i].tile].render.object = entities[i].id; if(entities[i].logic){ window[entities[i].logic.func].apply(null, entities[i].logic.data); } } drawGame(map); window.requestAnimationFrame(function(){ mainLoop(); }); }; function drawGame(map){ ctx.clearRect(0, 0, canvas.width, canvas.height); // We save all the entity data for later so the background colors don't get rendered on top var tileObjData = []; for(var y = 0; y < mapH; ++y){ for(var x = 0; x < mapW; ++x){ var currentPos = ((y*mapW)+x); ctx.fillStyle = map[currentPos].render.base; ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize); var thisObj = map[currentPos].render.object; if(thisObj !== false){ thisObj = entities[thisObj]; var originX = thisObj.xy.x; var originY = thisObj.xy.y; tileObjData.push( { id: thisObj.id, originX: originX, originY: originY, width: thisObj.width, height: thisObj.height, } ); } } } // Draw all the entities after the background tiles are drawn for(var i = 0; i < tileObjData.length; ++i){ drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height); } } // Draws the entity data function drawEntity(id, posX, posY, sizeX, sizeY){ var offX = posX + entities[id].speedX; var offY = posY + entities[id].speedY; ctx.fillStyle = '#00F'; ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY); entities[id].xy.x = offX; entities[id].xy.y = offY; } // Redraws the canvas with the browser framerate function mainLoop(){ drawGame(map); for(var i = 0; i < entities.length; ++i){ animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right); } window.requestAnimationFrame(function(){ mainLoop(); }); } // Sets the speed, direction, and collision detection of an entity function animateMove(id, up, down, left, right){ var prevTile = entities[id].tile; if(up){ var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y}; var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y}; if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){ entities[id].speedY = 0; } else{ entities[id].speedY = -1; } } else if(down){ var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1}; var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1}; if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){ entities[id].speedY = 0; } else{ entities[id].speedY = 1; } } else{ entities[id].speedY = 0; } if(left){ var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1}; var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y}; if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){ entities[id].speedX = 0; } else{ entities[id].speedX = -1; } } else if(right){ var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1}; var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y}; if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){ entities[id].speedX = 0; } else{ entities[id].speedX = 1; } } else{ entities[id].speedX = 0; } entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2)); map[entities[id].tile].render.object = id; if(prevTile !== entities[id].tile){ map[prevTile].render.object = false; } } ////////////////////////////////////// // THIS IS WHERE I'M HAVING TROUBLE // ////////////////////////////////////// // A function that can be used by an entity to move along a set path // id = The id of the entity using this function // path = An array of strings that determine the direction of movement for a single tile // originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently. // step = The current index of the path array function setPath(id, path, originPoint, step){ // Determine if the entity has travelled one tile from the origin var destX = Math.abs(entities[id].xy.x - originPoint.x); var destY = Math.abs(entities[id].xy.y - originPoint.y); if(destX >= tileSize || destY >= tileSize){ // Go to the next step in the path array step = step + 1; if(step >= path.length){ step = 0; } // Reset the origin to the current tile coordinates originPoint = entities[id].xy; } // Set the direction based on the current index of the path array switch(path[step]) { case 'up': entities[id].dir.up = true; entities[id].dir.down = false; entities[id].dir.left = false; entities[id].dir.right = false; break; case 'down': entities[id].dir.up = false; entities[id].dir.down = true; entities[id].dir.left = false; entities[id].dir.right = false; break; case 'left': entities[id].dir.up = false; entities[id].dir.down = false; entities[id].dir.left = true; entities[id].dir.right = false; break; case 'right': entities[id].dir.up = false; entities[id].dir.down = false; entities[id].dir.left = false; entities[id].dir.right = true; break; }; window.requestAnimationFrame(function(){ setPath(id, path, originPoint, step); }); } // Take a tile index and return x,y coordinates function tileToCoords(tile){ var yIndex = Math.floor(tile / mapW); var xIndex = tile - (yIndex * mapW); var y = yIndex * tileSize; var x = xIndex * tileSize; return {x:x, y:y}; } // Take x,y coordinates and return a tile index function coordsToTile(x, y){ var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize)); return tile; } // Generate a map array with a blank map and 4 walls function testMap(){ for(var i = 0; i < (mapH * mapW); ++i){ // Edges if ( // top i < mapW || // left (i % mapW) == 0 || // right ((i + 1) % mapW) == 0 || // bottom i > ((mapW * mapH) - mapW) ) { map.push( { id: i, render: { base: '#D35', object: false, sprite: false }, state: { passable: false } }, ); } else{ // Grass map.push( { id: i, render: { base: '#0C3', object: false, sprite: false }, state: { passable: true } }, ); } } } <!DOCTYPE html> <html> <head> <style> body{ background-color: #000; display: flex; align-items: center; justify-content: center; color: #FFF; font-size: 18px; padding: 0; margin: 0; } main{ width: 100%; max-width: 800px; margin: 10px auto; display: flex; align-items: flex-start; justify-content: center; flex-wrap: wrap; } .game{ width: 1000px; height: 1000px; position: relative; } canvas{ image-rendering: -moz-crisp-edges; image-rendering: -webkit-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; } .game canvas{ position: absolute; top: 0; left: 0; width: 800px; height: 800px; } </style> </head> <body> <main> <div class="game"> <canvas id="save" width="200" height="200" style="z-index: 1;"></canvas> </div> </main> </body> </html> The problem is with the setPath() function, and more specifically I think it's something with the originPoint variable. The idea is that setPath() moves the object one tile per path string, and originPoint should be the coordinates of the last tile visited (so it should only get updated once the object coordinates are one tile length away from the originPoint). Right now it only gets updated the first time and then stops. Hopefully someone can point out what I got wrong here.
Your condition to change the path direction I change it to have conditions for each direction, something like: if ((entities[id].dir.left && entities[id].xy.x <= tileSize) || (entities[id].dir.right && entities[id].xy.x >= tileSize*8) || (entities[id].dir.up && entities[id].xy.y <= tileSize) || (entities[id].dir.down && entities[id].xy.y >= tileSize*8)) { and the originPoint was just a reference you should do: originPoint = JSON.parse(JSON.stringify(entities[id].xy)); See the working code below // Canvas Element var canvas = null; // Canvas Draw var ctx = null; // Static Globals var tileSize = 16, mapW = 10, mapH = 10; // Instances of entities var entities = [ // A single entity that starts at tile 28, and uses the setPath() function { id: 0, tile: 28, xy: tileToCoords(28), width: 16, height: 24, speedX: 0, speedY: 0, logic: { func: 'setPath', // These are the parameters that go into the setPath() function data: [0, ['down', 'left', 'down', 'left', 'up', 'left', 'left', 'right', 'up', 'right', 'down','right', "up"], tileToCoords(28), 0] }, dir: {up:false, down:false, left:false, right:false} } ]; // Array for tile data var map = []; window.onload = function(){ // Populate the map array with a blank map and 4 walls testMap(); canvas = document.getElementById('save'); ctx = canvas.getContext("2d"); // Add all the entities to the map array and start their behavior for(var i = 0; i < entities.length; ++i){ map[entities[i].tile].render.object = entities[i].id; if(entities[i].logic){ window[entities[i].logic.func].apply(null, entities[i].logic.data); } } drawGame(map); window.requestAnimationFrame(function(){ mainLoop(); }); }; function drawGame(map){ ctx.clearRect(0, 0, canvas.width, canvas.height); // We save all the entity data for later so the background colors don't get rendered on top var tileObjData = []; for(var y = 0; y < mapH; ++y){ for(var x = 0; x < mapW; ++x){ var currentPos = ((y*mapW)+x); ctx.fillStyle = map[currentPos].render.base; ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize); var thisObj = map[currentPos].render.object; if(thisObj !== false){ thisObj = entities[thisObj]; var originX = thisObj.xy.x; var originY = thisObj.xy.y; tileObjData.push( { id: thisObj.id, originX: originX, originY: originY, width: thisObj.width, height: thisObj.height, } ); } } } // Draw all the entities after the background tiles are drawn for(var i = 0; i < tileObjData.length; ++i){ drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height); } } // Draws the entity data function drawEntity(id, posX, posY, sizeX, sizeY){ var offX = posX + entities[id].speedX; var offY = posY + entities[id].speedY; ctx.fillStyle = '#00F'; ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY); entities[id].xy.x = offX; entities[id].xy.y = offY; } // Redraws the canvas with the browser framerate function mainLoop(){ drawGame(map); for(var i = 0; i < entities.length; ++i){ animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right); } window.requestAnimationFrame(function(){ mainLoop(); }); } // Sets the speed, direction, and collision detection of an entity function animateMove(id, up, down, left, right){ var prevTile = entities[id].tile; if(up){ var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y}; var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y}; if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){ entities[id].speedY = 0; } else{ entities[id].speedY = -1; } } else if(down){ var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1}; var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1}; if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){ entities[id].speedY = 0; } else{ entities[id].speedY = 1; } } else{ entities[id].speedY = 0; } if(left){ var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1}; var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y}; if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){ entities[id].speedX = 0; } else{ entities[id].speedX = -1; } } else if(right){ var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1}; var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y}; if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){ entities[id].speedX = 0; } else{ entities[id].speedX = 1; } } else{ entities[id].speedX = 0; } entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2)); map[entities[id].tile].render.object = id; if(prevTile !== entities[id].tile){ map[prevTile].render.object = false; } } ////////////////////////////////////// // THIS IS WHERE I'M HAVING TROUBLE // ////////////////////////////////////// // A function that can be used by an entity to move along a set path // id = The id of the entity using this function // path = An array of strings that determine the direction of movement for a single tile // originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently. // step = The current index of the path array function setPath(id, path, originPoint, step){ if ((entities[id].dir.left && entities[id].xy.x <= originPoint.x - tileSize) || (entities[id].dir.right && entities[id].xy.x >= originPoint.x + tileSize) || (entities[id].dir.up && entities[id].xy.y <= originPoint.y - tileSize) || (entities[id].dir.down && entities[id].xy.y >= originPoint.y + tileSize)) { // Go to the next step in the path array step = step + 1; if(step >= path.length){ step = 0; } // Reset the origin to the current tile coordinates originPoint = JSON.parse(JSON.stringify(entities[id].xy)); } // Set the direction based on the current index of the path array switch(path[step]) { case 'up': entities[id].dir.up = true; entities[id].dir.down = false; entities[id].dir.left = false entities[id].dir.right = false; break; case 'down': entities[id].dir.up = false; entities[id].dir.down = true; entities[id].dir.left = false; entities[id].dir.right = false; break; case 'left': entities[id].dir.up = false; entities[id].dir.down = false; entities[id].dir.left = true; entities[id].dir.right = false; break; case 'right': entities[id].dir.up = false; entities[id].dir.down = false; entities[id].dir.left = false; entities[id].dir.right = true; break; }; window.requestAnimationFrame(function(){ setPath(id, path, originPoint, step); }); } // Take a tile index and return x,y coordinates function tileToCoords(tile){ var yIndex = Math.floor(tile / mapW); var xIndex = tile - (yIndex * mapW); var y = yIndex * tileSize; var x = xIndex * tileSize; return {x:x, y:y}; } // Take x,y coordinates and return a tile index function coordsToTile(x, y){ var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize)); return tile; } // Generate a map array with a blank map and 4 walls function testMap(){ for(var i = 0; i < (mapH * mapW); ++i){ // Edges if ( // top i < mapW || // left (i % mapW) == 0 || // right ((i + 1) % mapW) == 0 || // bottom i > ((mapW * mapH) - mapW) ) { map.push( { id: i, render: { base: '#D35', object: false, sprite: false }, state: { passable: false } }, ); } else{ // Grass map.push( { id: i, render: { base: '#0C3', object: false, sprite: false }, state: { passable: true } }, ); } } } <!DOCTYPE html> <html> <head> <style> body{ background-color: #000; display: flex; align-items: center; justify-content: center; color: #FFF; font-size: 18px; padding: 0; margin: 0; } main{ width: 100%; max-width: 800px; margin: 10px auto; display: flex; align-items: flex-start; justify-content: center; flex-wrap: wrap; } .game{ width: 1000px; height: 1000px; position: relative; } canvas{ image-rendering: -moz-crisp-edges; image-rendering: -webkit-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; } .game canvas{ position: absolute; top: 0; left: 0; width: 800px; height: 800px; } </style> </head> <body> <main> <div class="game"> <canvas id="save" width="200" height="200" style="z-index: 1;"></canvas> </div> </main> </body> </html>
As someone has already solved your bug... This is more than a solution to your problem as the real problem you are facing is complexity, long complex if statements using data structures representing the same information in different ways making it difficult to see simple errors in logic. On top of that you have some poor style habits that compound the problem. A quick fix will just mean you will be facing the next problem sooner. You need to write in a ways that reduces the chances of logic errors due to increasing complexity Style First style. Good style is very important Don't assign null to declared variables. JavaScript should not need to use null, the exception to the rule is that some C++ coders infected the DOM API with null returns because they did not understand JavaScipt (or was a cruel joke), and now we are stuck with null window is the default this (global this) and is seldom needed. Eg window.requestAnimationFrame is identical to just requestAnimationFrame and window.onload is identical to onload Don't pollute your code with inaccurate, redundant and/or obvious comments, use good naming to provide the needed information. eg: var map[]; has the comment // array of tile data Well really its an array that has data, who would have guessed, so the comment can be // tiles but then map is a rather ambiguous name. Remove the comment and give the variable a better name. The comment // Static Globals above some vars. Javascript does not have static as a type so the comment is wrong and the "global's" part is "duh..." Use const to declare constants, move all the magic numbers to the top and define them as named const. A name has meaning, an number in some code has next to no meaning. Don't assign listener to the event name, it is unreliable and can be hijacked or overwritten. Always use addEventListener to assign an event listener Be very careful with your naming. eg the function named coordsToTile is confusing as it does not return a tile, it returns a tile index, either change the function name to match the functions behavior, or change the behavior to match the name. Don't use redundant intermediate functions, examples: Your frame request requestAnimationFrame(function(){mainLoop()}); should skip the middle man and be requestAnimationFrame(mainLoop); You use Function.apply to call the function window[entities[i].logic.func].apply(null, entities[i].logic.data);. apply is used to bind context this to the call, you don't use this in the function so you don't need use the apply. eg window[entities[i].logic.func](...entities[i].logic.data); BTW being forced to use bracket notation to access a global is a sign of poor data structure. You should never do that. JavaScript has an unofficial idiomatic styles, you should try to write JS in this style. Some examples from your code else on the same line as closing } Space after if, else, for, function() and befor else, opening block { An id and an index are not the same, use idx or index for an index and id for an identifier Keep it simple The more complex you make your data structures the harder it is for you to maintain them. Structured Define objects to encapsulate and organize your data. A global config object, that is transprotable ie can converted be to and from JSON. it contains all the magic numbers, defaults, type descriptions, and what not needed in the game. Create a set of global utilities that do common repeated tasks, ie create coordinates, list of directions. Define object that encapsulate the settings and behaviors specific only to that object. Use polymorphic object design, meaning that different objects use named common behaviors and properties. In the example all drawable object have a function called draw that takes an argument ctx, all objects that can be updated have a function called update Example This example is a complete rewrite of your code and fixing your problem. It may be a little advanced, but it is only an example to look though an pick up some tips. A quick description of the objects used. Objects config is transportable config data testMap is an example map description tileMap does map related stuff Path Object encapsulating path logic Entity Object a single moving entity Tile Object representing a single tile game The game state manager Games have states, eg loading, intro, inPlay, gameOver etc. If you do not plan ahead and create a robust state manager you will find it very difficult to move from one state to the next I have included the core of a finite state manager. The state manager is responsible for updating and rendering. it is also responsible for all state changes. setTimeout(() => game.state = "setup", 0); // Will start the game const canvas = document.getElementById('save'); const ctx = canvas.getContext("2d"); const point = (x = 0, y = 0) => ({x,y}); const dirs = Object.assign( [point(0, -1), point(1), point(0,1), point(-1)], { // up, right, down, left "u": 0, // defines index for direction string characters "r": 1, "d": 2, "l": 3, strToDirIdxs(str) { return str.toLowerCase().split("").map(char => dirs[char]) }, } ); const config = { pathIdx: 28, pathTypes: { standard: "dulr", complex: "dulrldudlruldrdlrurdlurd", }, tile: {size: 16}, defaultTileName: "grass", entityTypes: { e: { speed: 1 / 32, // in fractions of a tile per frame color: "#00F", size: {x:16, y:24}, pathName: "standard", }, f: { speed: 1 / 16, // in fractions of a tile per frame color: "#08F", size: {x:18, y:18}, pathName: "complex", }, }, tileTypes: { grass: { style: {baseColor: "#0C3", object: false, sprite: false}, state: {passable: true} }, wall: { style: {baseColor: "#D35", object: false, sprite: false}, state: {passable: false} }, }, } const testMap = { displayChars: { " " : "grass", // what characters mean "#" : "wall", "E" : "grass", // also entity spawn "F" : "grass", // also entity spawn }, special: { // spawn enties and what not "E"(idx) { entities.push(new Entity(config.entityTypes.e, idx)) }, "F"(idx) { entities.push(new Entity(config.entityTypes.f, idx)) } }, map: // I double the width and ignor every second characters as text editors tend to make chars thinner than high // 0_1_2_3_4_5_6_7_8_9_ x coord "####################\n" + "##FF ## ##\n" + "## ## ##\n" + "## #### ##\n" + "## ##\n" + "## #### ##\n" + "## ##\n" + "## ##\n" + "## EE##\n" + "####################", // 0_1_2_3_4_5_6_7_8_9_ x coord } const entities = Object.assign([],{ update() { for (const entity of entities) { entity.update() } }, draw(ctx) { for (const entity of entities) { entity.draw(ctx) } }, }); const tileMap = { map: [], mapToIndex(x, y) { return x + y * tileMap.width }, pxToIndex(x, y) { return x / config.tile.size | 0 + (y / config.tile.size | 0) * tileMap.width }, tileByIdx(idx) { return tileMap.map[idx] }, tileByIdxDir(idx, dir) { return tileMap.map[idx + dir.x + dir.y * tileMap.width] }, idxByDir(dir) { return dir.x + dir.y * tileMap.width }, create(mapConfig) { tileMap.length = 0; const rows = mapConfig.map.split("\n"); tileMap.width = rows[0].length / 2 | 0; tileMap.height = rows.length; canvas.width = tileMap.width * config.tile.size; canvas.height = tileMap.height * config.tile.size; var x, y = 0; while (y < tileMap.height) { const row = rows[y]; for (x = 0; x < tileMap.width; x += 1) { const char = row[x * 2]; tileMap.map.push(new Tile(mapConfig.displayChars[char], x, y)); if (mapConfig.special[char]) { mapConfig.special[char](tileMap.mapToIndex(x, y)); } } y++; } }, update () {}, // stub draw(ctx) { for (const tile of tileMap.map) { tile.draw(ctx) } }, }; function Tile(typeName, x, y) { typeName = config.tileTypes[typeName] ? typeName : config.defaultTileName; const t = config.tileTypes[typeName]; this.idx = x + y * tileMap.width; this.coord = point(x * config.tile.size, y * config.tile.size); this.style = {...t.style}; this.state = {...t.state}; } Tile.prototype = { draw(ctx) { ctx.fillStyle = this.style.baseColor; ctx.fillRect(this.coord.x, this.coord.y, config.tile.size, config.tile.size); } }; function Path(pathName) { if (typeof config.pathTypes[pathName] === "string") { config.pathTypes[pathName] = dirs.strToDirIdxs(config.pathTypes[pathName]); } this.indexes = config.pathTypes[pathName]; this.current = -1; } Path.prototype = { nextDir(tileIdx) { var len = this.indexes.length; while (len--) { // make sure we dont loop forever const dirIdx = this.indexes[this.current]; if (dirIdx > - 1) { const canMove = tileMap.tileByIdxDir(tileIdx, dirs[dirIdx]).state.passable; if (canMove) { return dirs[dirIdx] } } this.current = (this.current + 1) % this.indexes.length; } } }; function Entity(type, tileIdx) { this.coord = point(); this.move = point(); this.color = type.color; this.speed = type.speed; this.size = {...type.size}; this.path = new Path(type.pathName); this.pos = this.nextTileIdx = tileIdx; this.traveled = 1; // unit dist between tiles 1 forces update to find next direction } Entity.prototype = { set dir(dir) { if (dir === undefined) { // dont move this.move.y = this.move.x = 0; this.nextTileIdx = this.tileIdx; } else { this.move.x = dir.x * config.tile.size; this.move.y = dir.y * config.tile.size; this.nextTileIdx = this.tileIdx + tileMap.idxByDir(dir); } }, set pos(tileIdx) { this.tileIdx = tileIdx; const tile = tileMap.map[tileIdx]; this.coord.x = tile.coord.x + config.tile.size / 2; this.coord.y = tile.coord.y + config.tile.size / 2; this.traveled = 0; }, draw(ctx) { const ox = this.move.x * this.traveled; const oy = this.move.y * this.traveled; ctx.fillStyle = this.color; ctx.fillRect(ox + this.coord.x - this.size.x / 2, oy + this.coord.y - this.size.y / 2, this.size.x, this.size.y) }, update(){ this.traveled += this.speed; if (this.traveled >= 1) { this.pos = this.nextTileIdx; this.dir = this.path.nextDir(this.tileIdx); } } }; const game = { currentStateName: undefined, currentState: undefined, set state(str) { if (game.states[str]) { if (game.currentState && game.currentState.end) { game.currentState.end() } game.currentStateName = str; game.currentState = game.states[str]; if (game.currentState.start) { game.currentState.start() } } }, states: { setup: { start() { tileMap.create(testMap); game.state = "play"; }, end() { requestAnimationFrame(game.render); // start the render loop delete game.states.setup; // MAKE SURE THIS STATE never happens again }, }, play: { render(ctx) { tileMap.update(); entities.update(); tileMap.draw(ctx); entities.draw(ctx); } } }, renderTo: ctx, startTime: undefined, time: 0, render(time) { if (game.startTime === undefined) { game.startTime = time } game.time = time - game.startTime; if (game.currentState && game.currentState.render) { game.currentState.render(game.renderTo) } requestAnimationFrame(game.render); } }; body{ background-color: #000; } canvas{ image-rendering: pixelated; position: absolute; top: 0; left: 0; width: 400px; height: 400px; } <canvas id="save" width="200" height="200" style="z-index: 1;"></canvas> Please Note that there are some running states that have not been tested and as such may have a typo. Also the tile map must be walled to contain entities or they will throw when they try to leave the playfield. The code is designed to run in the snippet. To make it work in a standard page add above the very first line setTimeout(() => game.state = "setup", 0); the line addEventListener(load", () = { and after the very last line add the line });
Display Absolute positioned images inside a div
i am trying to absolute position images at random positions inside a div. i'm not sure how to get the calculations right for 'top' and 'left' but the images on occasions display outside of the div. i want to also prevent the overlapping of the images. Any ideas would help (function() { //array of links to the images var images = ["http://via.placeholder.com/150/800", "http://via.placeholder.com/150/080", "http://via.placeholder.com/150/008", "http://via.placeholder.com/150/880" ]; //function to calculate a random integer var getRandomInt = function(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } //function to get a top and left value position for each image var pos = function() { var wrapWidth = document.getElementById("wrap"); wrapWidth = $("#wrap").width(); wrapHeight = $("#wrap").height(); // Image Position var xPos = getRandomInt(0, wrapWidth - 150); var yPos = getRandomInt(0, wrapHeight - 150); return { x: xPos + "px", y: yPos + "px" } } var displayImages = function(images) { var elementArray = []; for (var i = 0; i < images.length; i++) { var src = images[i]; elementArray[i] = '<img class="imagePos" style="top:' + pos().x + '; left:' + pos().y + ' " src="' + src + ' "/>'; } console.log(elementArray); elementArray.forEach(function(element) { console.log(element); $("#wrap").append(element); }); } displayImages(images); })(); <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <div id="IntTree"> <div id="header"> <h1>Interactive Tree</h1> </div> <div id="wrap"> </div> </div>
I'm assuming that you have some css resembling this: img { position: absolute; width:150; height:150; } Regarding your first issue, you appear to have your x and y assignments backwards in the bit where you're adding the elems to the array. Also, you're making 2 times as many calls to pos() as needed. That line should be: let position = pos(); elementArray[i] = '<img class="imagePos" style="top:'+position.y+'; left:'+position.x+' " src="'+src+' "/>'; For the second issue, you need to check for each image whether any of the corners overlap a different image. The easy way to achieve this by adding an array to track the positions you've already used, and comparing against the items in the array for subsequent position calculations. (function (){ //array of links to the images var images = ["http://via.placeholder.com/150/800", "http://via.placeholder.com/150/080", "http://via.placeholder.com/150/008", "http://via.placeholder.com/150/880" ]; //function to calculate a random integer var getRandomInt = function (min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // array to track previous positions of images var positions = []; //function to get a top and left value position for each image var pos = function (){ var wrapWidth = $("#wrap").width(); var wrapHeight = $("#wrap").height(); // Image Position var xPos = getRandomInt(0, wrapWidth - 150); var yPos = getRandomInt(0, wrapHeight - 150); var overlapX = true; var overlapY = true; while(overlapX && overlapY) { overlapX = false; overlapY = false; for(var i = 0; i < positions.length; i++) { // check if x coord is inside previously placed image if ( (xPos > positions[i].x && xPos < positions[i].x+150) || (xPos+150 > positions[i].x && (xPos+150) < positions[i].x+150) ){ overlapX = true; } // check if y coord is inside previously placed image if( (yPos > positions[i].y && yPos < positions[i].y+150) || (yPos+150 > positions[i].y && yPos+150 < positions[i].y+150) ) { overlapY = true; } } if (overlapX) { xPos = getRandomInt(0, wrapWidth - 150); } if (overlapY) { yPos = getRandomInt(0, wrapHeight - 150); } } positions.push({x:xPos,y:yPos}); return { x: xPos + "px", y: yPos + "px" } } var displayImages = function(images){ var elementArray = []; for (var i = 0; i < images.length; i++) { var src = images[i]; let position = pos(); elementArray[i] = '<img class="imagePos" style="top:'+position.y+'; left:'+position.x+' " src="'+src+' "/>'; } elementArray.forEach(function(element) { $("#wrap").append(element); }); } displayImages(images); })();
Transforming img - white dots appear on chronium
I am trying to get 3d transform effect of the bacground image on mouse move. I have checked f.e. jquery.plate tilt.js and many others plugins, BUT, each of them has problem on chronium browsers like Chrome or Opera (it works fine even on IE11 -.-) See the attachment and please adwise what is that and if it is fixable? The "dots" appear on mouse move (when background moves) randomly but in line within image. (function($) { 'use strict'; var namespace = 'jquery-plate'; function Plate($element, options) { this.config(options); this.$container = $element; if (this.options.element) { if (typeof this.options.element === 'string') { this.$element = this.$container.find(this.options.element); } else { this.$element = $(this.options.element); } } else { this.$element = $element; } this.originalTransform = this.$element.css('transform'); this.$container .on('mouseenter.' + namespace, this.onMouseEnter.bind(this)) .on('mouseleave.' + namespace, this.onMouseLeave.bind(this)) .on('mousemove.' + namespace, this.onMouseMove.bind(this)); } Plate.prototype.config = function(options) { this.options = $.extend({ inverse: false, perspective: 500, maxRotation: 10, animationDuration: 200 }, this.options, options); }; Plate.prototype.destroy = function() { this.$element.css('transform', this.originalTransform); this.$container.off('.' + namespace); }; Plate.prototype.update = function(offsetX, offsetY, duration) { var rotateX; var rotateY; if (offsetX || offsetX === 0) { var height = this.$container.outerHeight(); var py = (offsetY - height / 2) / (height / 2); rotateX = this.round(this.options.maxRotation * -py); } else { rotateY = 0; } if (offsetY || offsetY === 0) { var width = this.$container.outerWidth(); var px = (offsetX - width / 2) / (width / 2); rotateY = this.round(this.options.maxRotation * px); } else { rotateX = 0; } if (this.options.inverse) { rotateX *= -1; rotateY *= -1; } if (duration) { this.animate(rotateX, rotateY, duration); } else if (this.animation && this.animation.remaining) { this.animation.targetX = rotateX; this.animation.targetY = rotateY; } else { this.transform(rotateX, rotateY); } }; Plate.prototype.reset = function(duration) { this.update(null, null, duration); }; Plate.prototype.transform = function(rotateX, rotateY) { this.currentX = rotateX; this.currentY = rotateY; this.$element.css('transform', (this.originalTransform && this.originalTransform !== 'none' ? this.originalTransform + ' ' : '') + 'perspective(' + this.options.perspective + 'px) ' + 'rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg)' ); }; Plate.prototype.animate = function(rotateX, rotateY, duration) { if (duration) { this.animation = this.animation || {}; var remaining = this.animation.remaining; this.animation.time = performance.now(); this.animation.remaining = duration || null; this.animation.targetX = rotateX; this.animation.targetY = rotateY; if (!remaining) { requestAnimationFrame(this.onAnimationFrame.bind(this)); } } else { this.transform(rotateX, rotateY); } }; Plate.prototype.round = function(number) { return Math.round(number * 1000) / 1000; }; Plate.prototype.offsetCoords = function(event) { var offset = this.$container.offset(); return { x: event.pageX - offset.left, y: event.pageY - offset.top }; }; Plate.prototype.onAnimationFrame = function(timestamp) { this.animation = this.animation || {}; var delta = timestamp - (this.animation.time || 0); this.animation.time = timestamp; var duration = this.animation.remaining || 0; var percent = Math.min(delta / duration, 1); var currentX = this.currentX || 0; var currentY = this.currentY || 0; var targetX = this.animation.targetX || 0; var targetY = this.animation.targetY || 0; var rotateX = this.round(currentX + (targetX - currentX) * percent); var rotateY = this.round(currentY + (targetY - currentY) * percent); this.transform(rotateX, rotateY); var remaining = duration - delta; this.animation.remaining = remaining > 0 ? remaining : null; if (remaining > 0) { requestAnimationFrame(this.onAnimationFrame.bind(this)); } }; Plate.prototype.onMouseEnter = function(event) { var offset = this.offsetCoords(event); this.update(offset.x, offset.y, this.options.animationDuration); }; Plate.prototype.onMouseLeave = function(event) { this.reset(this.options.animationDuration); }; Plate.prototype.onMouseMove = function(event) { var offset = this.offsetCoords(event); this.update(offset.x, offset.y); }; $.fn.plate = function(options) { return this.each(function() { var $element = $(this); var plate = $element.data(namespace); if (options === 'remove') { plate.destroy(); $element.data(namespace, null); } else { if (!plate) { plate = new Plate($element, options); $element.data(namespace, plate); plate.reset(); } else { plate.config(options); } } }); }; })(jQuery); $('#ab12cd').plate() <div id="ab12cd" styles="width:100%;height:100%"> <img src="http://eskipaper.com/images/dark-background-8.jpg" /> </div> <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script> // please open in new window, most visible effect you cen see if you move mouse bottom left/right
Prevent Fabric js Objects from scaling out of the canvas boundary
I have been trying to keep an object (constructed in fabric js over a canvas) inside the boundaries at all the times. It has been achieved at moving and rotating it. I took help from Move object within canvas boundary limit for achieving this. But when I start to scale the object, it simply keeps on going out of boundary. I do not understand what has to be done to keep it inside the boundary only, even while scaling. Please help me with a code to prevent this behavior. It would be great if you can attach a demo too. <html> <head> <title>Basic usage</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.3/fabric.min.js"></script> </head> <body> <canvas id="canvas" style= "border: 1px solid black" height= 480 width = 360></canvas> <script> var canvas = new fabric.Canvas('canvas'); canvas.add(new fabric.Circle({ radius: 30, fill: '#f55', top: 100, left: 100 })); canvas.item(0).set({ borderColor: 'gray', cornerColor: 'black', cornerSize: 12, transparentCorners: true }); canvas.setActiveObject(canvas.item(0)); canvas.renderAll(); canvas.on('object:moving', function (e) { var obj = e.target; // if object is too big ignore if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){ return; } obj.setCoords(); // top-left corner if(obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0){ obj.top = Math.max(obj.top, obj.top-obj.getBoundingRect().top); obj.left = Math.max(obj.left, obj.left-obj.getBoundingRect().left); } // bot-right corner if(obj.getBoundingRect().top+obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left+obj.getBoundingRect().width > obj.canvas.width){ obj.top = Math.min(obj.top, obj.canvas.height-obj.getBoundingRect().height+obj.top-obj.getBoundingRect().top); obj.left = Math.min(obj.left, obj.canvas.width-obj.getBoundingRect().width+obj.left-obj.getBoundingRect().left); } }); </script> </body> </html> My demo is attached here. : https://jsfiddle.net/3v0cLaLk/
I was able to solve the problem as follows: var canvas = new fabric.Canvas('canvas'); canvas.add(new fabric.Circle({ radius: 30, fill: '#f55', top: 100, left: 100 })); canvas.item(0).set({ borderColor: 'gray', cornerColor: 'black', cornerSize: 12, transparentCorners: true }); canvas.setActiveObject(canvas.item(0)); canvas.renderAll(); canvas.on('object:moving', function (e) { var obj = e.target; // if object is too big ignore if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){ return; } obj.setCoords(); // top-left corner if(obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0){ obj.top = Math.max(obj.top, obj.top-obj.getBoundingRect().top); obj.left = Math.max(obj.left, obj.left-obj.getBoundingRect().left); } // bot-right corner if(obj.getBoundingRect().top+obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left+obj.getBoundingRect().width > obj.canvas.width){ obj.top = Math.min(obj.top, obj.canvas.height-obj.getBoundingRect().height+obj.top-obj.getBoundingRect().top); obj.left = Math.min(obj.left, obj.canvas.width-obj.getBoundingRect().width+obj.left-obj.getBoundingRect().left); } }); var left1 = 0; var top1 = 0 ; var scale1x = 0 ; var scale1y = 0 ; var width1 = 0 ; var height1 = 0 ; canvas.on('object:scaling', function (e){ var obj = e.target; obj.setCoords(); var brNew = obj.getBoundingRect(); if (((brNew.width+brNew.left)>=obj.canvas.width) || ((brNew.height+brNew.top)>=obj.canvas.height) || ((brNew.left<0) || (brNew.top<0))) { obj.left = left1; obj.top=top1; obj.scaleX=scale1x; obj.scaleY=scale1y; obj.width=width1; obj.height=height1; } else{ left1 =obj.left; top1 =obj.top; scale1x = obj.scaleX; scale1y=obj.scaleY; width1=obj.width; height1=obj.height; } }); <html> <head> <title>Basic usage</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.3/fabric.min.js"></script> </head> <body> <canvas id="canvas" style= "border: 1px solid black" height= 480 width = 360></canvas> </body> </html>
You can set on object modified listener and check if object is out of bounds. If so, then restore it to its original state. this.canvas.on('object:modified', function (options: any) { let obj = options.target; let boundingRect = obj.getBoundingRect(true); if (boundingRect.left < 0 || boundingRect.top < 0 || boundingRect.left + boundingRect.width > scope.canvas.getWidth() || boundingRect.top + boundingRect.height > scope.canvas.getHeight()) { obj.top = obj._stateProperties.top; obj.left = obj._stateProperties.left; obj.angle = obj._stateProperties.angle; obj.scaleX = obj._stateProperties.scaleX; obj.scaleY = obj._stateProperties.scaleY; obj.setCoords(); obj.saveState(); } });
If you want to perform a real time prevention, you should use object:scaling event, as object:modified is only triggered at the end of the transformation. 1) Add event handler to canvas: this.canvas.on('object:scaling', (e) => this._handleScaling(e)); 2) In the handler function, get the old and the new object's bounding rect: _handleScaling(e) { var obj = e.target; var brOld = obj.getBoundingRect(); obj.setCoords(); var brNew = obj.getBoundingRect(); 3) For each border, check if object has scaled beyond the canvas boundaries and compute its left, top and scale properties: // left border // 1. compute the scale that sets obj.left equal 0 // 2. compute height if the same scale is applied to Y (we do not allow non-uniform scaling) // 3. compute obj.top based on new height if(brOld.left >= 0 && brNew.left < 0) { let scale = (brOld.width + brOld.left) / obj.width; let height = obj.height * scale; let top = ((brNew.top - brOld.top) / (brNew.height - brOld.height) * (height - brOld.height)) + brOld.top; this._setScalingProperties(0, top, scale); } 4) Similar code for the other borders: // top border if(brOld.top >= 0 && brNew.top < 0) { let scale = (brOld.height + brOld.top) / obj.height; let width = obj.width * scale; let left = ((brNew.left - brOld.left) / (brNew.width - brOld.width) * (width - brOld.width)) + brOld.left; this._setScalingProperties(left, 0, scale); } // right border if(brOld.left + brOld.width <= obj.canvas.width && brNew.left + brNew.width > obj.canvas.width) { let scale = (obj.canvas.width - brOld.left) / obj.width; let height = obj.height * scale; let top = ((brNew.top - brOld.top) / (brNew.height - brOld.height) * (height - brOld.height)) + brOld.top; this._setScalingProperties(brNew.left, top, scale); } // bottom border if(brOld.top + brOld.height <= obj.canvas.height && brNew.top + brNew.height > obj.canvas.height) { let scale = (obj.canvas.height - brOld.top) / obj.height; let width = obj.width * scale; let left = ((brNew.left - brOld.left) / (brNew.width - brOld.width) * (width - brOld.width)) + brOld.left; this._setScalingProperties(left, brNew.top, scale); } 5) If object's BoundingRect has crossed canvas boundaries, fix its position and scale: if(brNew.left < 0 || brNew.top < 0 || brNew.left + brNew.width > obj.canvas.width || brNew.top + brNew.height > obj.canvas.height) { obj.left = this.scalingProperties['left']; obj.top = this.scalingProperties['top']; obj.scaleX = this.scalingProperties['scale']; obj.scaleY = this.scalingProperties['scale']; obj.setCoords(); } else { this.scalingProperties = null; } } 6) Finally, when setting the scaling properties, we have to stick with the smallest scale in case the object has crossed more than one border: _setScalingProperties(left, top, scale) { if(this.scalingProperties == null || this.scalingProperties['scale'] > scale) { this.scalingProperties = { 'left': left, 'top': top, 'scale': scale }; } }
Below is the code for blocking the coordinates of any object outside the canvas area from all directions canvas.on('object:modified', function (data) { var currentObject = data.target; var tempObject = angular.copy(data.target); var canvasMaxWidth = canvas.width - 20, canvasMaxHeight = canvas.height - 20; var actualWidth = currentObject.getBoundingRect().width, actualHeight = currentObject.getBoundingRect().height; if (actualHeight > canvasMaxHeight) { currentObject.scaleToHeight(canvasMaxHeight); currentObject.setCoords(); canvas.renderAll(); if (tempObject.scaleX < currentObject.scaleX) { currentObject.scaleX = tempObject.scaleX; currentObject.setCoords(); canvas.renderAll(); } if (tempObject.scaleY < currentObject.scaleY) { currentObject.scaleY = tempObject.scaleY; currentObject.setCoords(); canvas.renderAll(); } if (currentObject.getBoundingRectHeight() < canvasMaxHeight - 50) { currentObject.scaleX = (currentObject.scaleX * canvasMaxHeight) / (currentObject.scaleX * currentObject.width); currentObject.setCoords(); canvas.renderAll(); } } if (actualWidth > canvasMaxWidth) { currentObject.scaleToWidth(canvasMaxWidth); obj.setCoords(); canvas.renderAll(); if (tempObject.scaleX < currentObject.scaleX) { currentObject.scaleX = tempObject.scaleX; currentObject.setCoords(); canvas.renderAll(); } if (tempObject.scaleY < currentObject.scaleY) { currentObject.scaleY = tempObject.scaleY; currentObject.setCoords(); canvas.renderAll(); } } obj.setCoords(); canvas.renderAll(); });
I was able to block movement outside of boundaries using the Bounding box in the following way using the last version of Fabric ("fabric": "^4.6.0") & Typescript: private boundingBox: fabric.Rect = null; this.setBoundingBox(width, height); private setBoundingBox(width: number, height: number) { this.boundingBox = new fabric.Rect({ name: OBJECT_TYPE.BOUNDING_BOX, fill: DEFINITIONS.BG_COLOR, width: width, height: height, hasBorders: false, hasControls: false, lockMovementX: true, lockMovementY: true, selectable: false, evented: false, stroke: 'red', }); this._canvas.add(this.boundingBox); } this._canvas.on('object:moving', (e) => { console.log('object:moving'); this._avoidObjectMovingOutsideOfBoundaries(e); }); private _avoidObjectMovingOutsideOfBoundaries(e: IEvent) { let obj = e.target; const top = obj.top; const bottom = top + obj.height; const left = obj.left; const right = left + obj.width; const topBound = this.boundingBox.top; const bottomBound = topBound + this.boundingBox.height; const leftBound = this.boundingBox.left; const rightBound = leftBound + this.boundingBox.width; obj.left = Math.min(Math.max(left, leftBound), rightBound - obj.width); obj.top = Math.min(Math.max(top, topBound), bottomBound - obj.height); return obj; } Any additional extensions for Scaling objects are welcome.
canvas.on('object:scaling', function (e) { var obj = e.target; obj.setCoords(); let top = obj.getBoundingRect().top; let left = obj.getBoundingRect().left; let height = obj.getBoundingRect().height; let width = obj.getBoundingRect().width; // restrict scaling below bottom of canvas if (top + height > CANVAS_HEIGHT) { obj.scaleY = 1; obj.setCoords(); let h = obj.getScaledHeight(); obj.scaleY = (CANVAS_HEIGHT - top) / h; obj.setCoords(); canvas.renderAll(); obj.lockScalingX = true; obj.lockScalingY = true; obj.lockMovementX = true; obj.lockMovementY = true; } // restrict scaling above top of canvas if (top < 0) { obj.scaleY = 1; obj.setCoords(); let h = obj.getScaledHeight(); obj.scaleY = (height + top) / h; obj.top = 0; obj.setCoords(); canvas.renderAll(); obj.lockScalingX = true; obj.lockScalingY = true; obj.lockMovementX = true; obj.lockMovementY = true; } // restrict scaling over right of canvas if (left + width > CANVAS_WIDTH) { obj.scaleX = 1; obj.setCoords(); let w = obj.getScaledWidth(); obj.scaleX = (CANVAS_WIDTH - left) / w; obj.setCoords(); canvas.renderAll(); obj.lockScalingX = true; obj.lockScalingY = true; obj.lockMovementX = true; obj.lockMovementY = true; } // restrict scaling over left of canvas if (left < 0) { obj.scaleX = 1; obj.setCoords(); let w = obj.getScaledWidth(); obj.scaleX = (width + left) / w; obj.left = 0; obj.setCoords(); canvas.renderAll(); obj.lockScalingX = true; obj.lockScalingY = true; obj.lockMovementX = true; obj.lockMovementY = true; } }); canvas.on('object:modified', function (event) { // after text object is done with modifing e.g. resizing or moving if (!!event.target) { event.target.lockScalingX = false; event.target.lockScalingY = false; event.target.lockMovementX = false; event.target.lockMovementY = false; } })
Using Raphael.setViewBox zoom/pan to show entire contents of set / paper
My objective is to create some code that will show (no animation required) a set of objects as large as possible, in the center of the paper. My current attempt gets the bounding box of a set of objects or paper and returns dimensions which is then used by setViewBox. Unfortunately it just doesn't work and I have no idea why. Here is my code function GetContainingViewBoxFor(object) { var maxValues = { x: 0, y: 0 }; var minValues = { x: 0, y: 0 }; //Find max and min points object.forEach(function (el) { if (el.data("background")) return; var bbox = el.getBBox(); if (bbox.y < minValues.y) minValues.y = bbox.y; if (bbox.y2 < minValues.y) minValues.y = bbox.y2; if (bbox.y > maxValues.y) maxValues.y = bbox.y; if (bbox.y2 > maxValues.y) maxValues.y = bbox.y2; if (bbox.x < minValues.x) minValues.x = bbox.x; if (bbox.x2 < minValues.x) minValues.x = bbox.x2; if (bbox.x > maxValues.x) maxValues.x = bbox.x; if (bbox.x2 > maxValues.x) maxValues.x = bbox.x2; }); //Padding var padding = 50; maxValues.x += padding; minValues.x -= padding; maxValues.y += padding; minValues.y -= padding; var w = maxValues.x - minValues.x; var h = maxValues.y - minValues.y; var cviewBox = {X:minValues.x - paper.width,Y:minValues.y,width:w,height:h}; return cviewBox; } And it is used like so: var containingViewBox = GetContainingViewBoxFor(paper); paper.setViewBox(containingViewBox.X, containingViewBox.Y, containingViewBox.width, containingViewBox.height, false);