问题
I have two objects a parent (red) and a child (blue). The parent object is fixed and can't be moved, only the child object is movable and the child is always bigger than the parent. In whatever way the child object is moved it should always be contained inside the child, which means we should never see the red rectangle.
Demo: https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-7nt7q
I know there are solutions to contain an object within the canvas or other object boundaries (ex. Move object within canvas boundary limit) which mainly force the top/right/bottom/left values to not exceed the parent values, but here we have the case of two rotated objects by the same degree.
I have a real-life scenario when a user uploads a photo to a frame container. The photo is normally always bigger than the frame container. The user can move the photo inside the frame, but he should not be allowed to create any empty spaces, the photo should always be contained inside the photo frame.
回答1:
I would go with a pure canvas (no fabricjs), do it from scratch that way you understand well the problem you are facing, then if you need it that same logic should be easily portable to any library.
You have some rules:
- The parent object is fixed and can't be moved,
- The child object is movable.
- The child is always bigger than the parent.
- The child object is always constrained by the parent.
So my idea is to get all four corners, that way on the move we can use those coordinates to determine if it can be moved to the new location or not, the code should be easy to follow, but ask if you have any concerns.
I'm using the ray-casting algorithm:
https://github.com/substack/point-in-polygon/blob/master/index.js
With that, all we need to do is check that the corners of the child are not inside the parent and that the parent is inside the child, that is all.
I'm no expert with FabricJS so my best might not be much...
but below is my attempt to get your code going.
<canvas id="canvas" width="500" height="350"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
var canvas = new fabric.Canvas("canvas");
canvas.stateful = true;
function getCoords(rect) {
var x = rect.left;
var y = rect.top;
var angle = (rect.angle * Math.PI) / 180;
var coords = [{ x, y }];
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.height * Math.cos(angle);
y += rect.height * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
return coords;
}
function inside(p, vs) {
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect =
yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
var parent = new fabric.Rect({
width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
});
var pCoords = getCoords(parent);
var child = new fabric.Rect({
width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
});
canvas.add(parent);
canvas.add(child);
canvas.on("object:moving", function (e) {
var cCoords = getCoords(e.target);
var inBounds = true;
cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
if (inBounds) {
e.target.setCoords();
e.target.saveState();
e.target.set("fill", "rgba(0,0,255,0.9)");
} else {
e.target.set("fill", "black");
e.target.animate({
left: e.target._stateProperties.left,
top: e.target._stateProperties.top
},{
duration: 500,
onChange: canvas.renderAll.bind(canvas),
easing: fabric.util.ease["easeInBounce"],
onComplete: function() {
e.target.set("fill", "rgba(0,0,255,0.9)");
}
});
}
});
</script>
That code is on sandbox as well:
https://codesandbox.io/s/force-contain-of-object-inside-another-object-fabric-js-dnvb5
It certainly is nice not to worry about coding all the click/hold/drag fabric makes that real easy...
I was experimenting with FabricJS and there a nice property of the canvas
(canvas.stateful = true;
)
that allows us to keep track of where we've been, and if we go out of bounds we can revert that movement, also playing with animate that gives the user visual feedback that the movement is not allowed.
Here is another version without animation:
<canvas id="canvas" width="500" height="350"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<script>
var canvas = new fabric.Canvas("canvas");
canvas.stateful = true;
function getCoords(rect) {
var x = rect.left;
var y = rect.top;
var angle = (rect.angle * Math.PI) / 180;
var coords = [{ x, y }];
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.height * Math.cos(angle);
y += rect.height * Math.sin(angle);
coords.push({ x, y });
angle += Math.PI / 2;
x += rect.width * Math.cos(angle);
y += rect.width * Math.sin(angle);
coords.push({ x, y });
return coords;
}
function inside(p, vs) {
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect =
yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
var parent = new fabric.Rect({
width: 150, height: 100, left: 200, top: 50, angle: 25, selectable: false, fill: "red"
});
var pCoords = getCoords(parent);
var child = new fabric.Rect({
width: 250, height: 175, left: 180, top: 10, angle: 25, hasControls: false, fill: "rgba(0,0,255,0.9)"
});
canvas.add(parent);
canvas.add(child);
canvas.on("object:moving", function (e) {
var cCoords = getCoords(e.target);
var inBounds = true;
cCoords.forEach(c => { if (inside(c, pCoords)) inBounds = false; });
pCoords.forEach(c => { if (!inside(c, cCoords)) inBounds = false; });
if (inBounds) {
e.target.setCoords();
e.target.saveState();
} else {
e.target.left = e.target._stateProperties.left;
e.target.top = e.target._stateProperties.top;
}
});
</script>
This algorithm also opens the door for other shapes as well, here is a hexagon version:
https://raw.githack.com/heldersepu/hs-scripts/master/HTML/canvas_contained2.html
回答2:
you can just create the new Class
, with your object
inside, and do all actions only with parent
of the children
, something like this:
fabric.RectWithRect = fabric.util.createClass(fabric.Rect, {
type: 'rectWithRect',
textOffsetLeft: 0,
textOffsetTop: 0,
_prevObjectStacking: null,
_prevAngle: 0,
minWidth: 50,
minHeight: 50,
_currentScaleFactorX: 1,
_currentScaleFactorY: 1,
_lastLeft: 0,
_lastTop: 0,
recalcTextPosition: function () {
//this.insideRect.setCoords();
const sin = Math.sin(fabric.util.degreesToRadians(this.angle))
const cos = Math.cos(fabric.util.degreesToRadians(this.angle))
const newTop = sin * this.insideRectOffsetLeft + cos * this.insideRectOffsetTop
const newLeft = cos * this.insideRectOffsetLeft - sin * this.insideRectOffsetTop
const rectLeftTop = this.getPointByOrigin('left', 'top')
this.insideRect.set('left', rectLeftTop.x + newLeft)
this.insideRect.set('top', rectLeftTop.y + newTop)
this.insideRect.set('width', this.width - 40)
this.insideRect.set('height', this.height - 40)
this.insideRect.set('scaleX', this.scaleX)
this.insideRect.set('scaleY', this.scaleY)
},
initialize: function (textOptions, rectOptions) {
this.callSuper('initialize', rectOptions)
this.insideRect = new fabric.Rect({
...textOptions,
dirty: false,
objectCaching: false,
selectable: false,
evented: false,
fragmentType: 'rectWidthRect'
});
canvas.bringToFront(this.insideRect);
this.insideRect.width = this.width - 40;
this.insideRect.height = this.height - 40;
this.insideRect.left = this.left + 20;
this.insideRect.top = this.top + 20;
this.insideRectOffsetLeft = this.insideRect.left - this.left
this.insideRectOffsetTop = this.insideRect.top - this.top
this.on('moving', function(e){
this.recalcTextPosition();
})
this.on('rotating',function(){
this.insideRect.rotate(this.insideRect.angle + this.angle - this._prevAngle)
this.recalcTextPosition()
this._prevAngle = this.angle
})
this.on('scaling', function(fEvent){
this.recalcTextPosition();
});
this.on('added', function(){
this.canvas.add(this.insideRect)
});
this.on('removed', function(){
this.canvas.remove(this.insideRect)
});
this.on('mousedown:before', function(){
this._prevObjectStacking = this.canvas.preserveObjectStacking
this.canvas.preserveObjectStacking = true
});
this.on('deselected', function(){
this.canvas.preserveObjectStacking = this._prevObjectStacking
});
}
});
and then just add your element
to your canvas
as usual:
var rectWithRect = new fabric.RectWithRect(
{
fill: "red",
}, // children rect options
{
left:100,
top:100,
width: 300,
height: 100,
dirty: false,
objectCaching: false,
strokeWidth: 0,
fill: 'blue'
} // parent rect options
);
canvas.add(rectWithRect);
by the way, you can use method like this to create nested
elements, text with background and other.
Codesandbox DEMO
来源:https://stackoverflow.com/questions/60822474/contain-a-rotated-object-inside-another-rotated-object-fabricjs