FabricJS - Object is removed if top left corner is moved out of viewport - fabricjs

I have a canvas. The users are able to draw rectangles in it using fabricjs. The second feature is that you are able to move around in the canvas (called panning). When I am in the drawing mode and than start to pan around, the rectangle disappears if I am moving the top left corner of the rectangle out of the canvas. If you move the viewport, so that the top left corner is in the canvas back again, the rectangle becomes visible again. If you move other corners of the rectangle out of the canvas, the rectangle stays visible.
Is there a way to prevent that behavior?
Steps to reproduce:
use snippet below
click the "rect" button
hold the left mouse key and draw a new rectangle (keep the mouse key pressed) - this will create a yellow rect
press and hold the "alt" key on your keyboard and move the mouse (the mouse key is still pressed) - you are panning around now
move the view, so that the top left corner of the rect will be outside the canvas - the yellow rect will disappear
Goal:
The rect will stay visible even if I am moving the top left corner out of the canvas.
(function() {
var drawMode = false
var isDrawing = false
var isPanning = false
document.getElementById("rect").onclick = function() {
drawMode = !drawMode
document.getElementById("rect").style["background-color"] = drawMode ? "lightblue" : "lightgrey"
}
var canvas = new fabric.Canvas("canvas")
canvas.on("mouse:down", function(opt) {
var evt = opt.e
this.selection = false
if (drawMode) {
isDrawing = true
this.startPosX = evt.clientX
this.startPosY = evt.clientY
this.drawRect = new fabric.Rect({
left: evt.clientX,
top: evt.clientY,
fill: "yellow",
})
canvas.add(this.drawRect)
} else if (evt.altKey === true) {
isPanning = true
this.lastPosX = evt.clientX
this.lastPosY = evt.clientY
}
})
canvas.on("mouse:move", function(opt) {
var evt = opt.e
if (evt.altKey === true && isPanning) {
var vpt = this.viewportTransform
if (this.lastPosX) vpt[4] += evt.clientX - this.lastPosX
if (this.lastPosY) vpt[5] += evt.clientY - this.lastPosY
this.requestRenderAll()
this.lastPosX = evt.clientX
this.lastPosY = evt.clientY
} else if (isDrawing) {
this.drawRect.set({
width: evt.clientX - this.startPosX,
height: evt.clientY - this.startPosY,
})
canvas.renderAll()
}
})
canvas.on("mouse:up", function(opt) {
// on mouse up we want to recalculate new interaction
// for all objects, so we call setViewportTransform
var evt = opt.e
this.setViewportTransform(this.viewportTransform)
isPanning = false
this.selection = true
if (isDrawing) {
this.drawRect.set({
width: evt.clientX - this.startPosX,
height: evt.clientY - this.startPosY,
fill: "orange",
})
isDrawing = false
}
})
document.addEventListener("keydown", function(opt) {
if (opt.key === "Alt") {
if (!isPanning) isPanning = true
if (drawMode) drawMode = false
}
})
document.addEventListener("keyup", function(opt) {
if (opt.key === "Alt") {
if (isPanning) isPanning = false
}
})
})()
<html>
<body style="margin: 0">
<canvas id="canvas" width=500 height=300 style="border: 1px solid blue"></canvas>
<h3>Tools</h3>
<button id="rect" style="background-color: lightgrey">rect</button>
<h3>Features</h3>
<ul>
<li>draw rectangle (activate draw mode with above button</li>
<li>move around (need to hold alt-key)</li>
</ul>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.4.0/fabric.min.js"></script>
</body>
</html>

Related

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.

Change Toooltip Color Javascript

Looking to change the color of my tooltip but not having much success. Is this a simple need for a color onto the dojo.style command? Here is my code so far
// create node for the tooltip
var tip = "Click on problem location";
var tooltip = dojo.create("div", { "class": "tooltip", "innerHTML": tip }, map.container);
dojo.style(tooltip, "position", "fixed");
// update the tooltip as the mouse moves over the map
dojo.connect(map, "onMouseMove", function(evt) {
var px, py;
if (evt.clientX || evt.pageY) {
px = evt.clientX;
py = evt.clientY;
} else {
px = evt.clientX + dojo.body().scrollLeft - dojo.body().clientLeft;
py = evt.clientY + dojo.body().scrollTop - dojo.body().clientTop;
}
// dojo.style(tooltip, "display", "none");
tooltip.style.display = "none";
dojo.style(tooltip, { left: (px + 15) + "px", top: (py) + "px" });
// dojo.style(tooltip, "display", "");
tooltip.style.display = "";
// console.log("updated tooltip pos.");
});
// hide the tooltip the cursor isn't over the map
dojo.connect(map, "onMouseOut", function(evt){
tooltip.style.display = "none";
});

How to make canvas responsive using Phaser 3?

Previously I was working on Phaser 2 but now I need to switch to Phaser 3.
I tried to make the canvas responsive with ScaleManager but it is not working.
I think some of the methods changed but I didn't find any help to rescale the stage full screen.
var bSize = {
bWidth: window.innerWidth ||
root.clientWidth ||
body.clientWidth,
bHeight: window.innerHeight ||
root.clientHeight ||
body.clientHeight,
};
var game;
var canvas = document.getElementById("canvas");
function create() {
// Scaling options
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
// Have the game centered horizontally
game.scale.pageAlignHorizontally = true;
// And vertically
game.scale.pageAlignVertically = true;
// Screen size will be set automatically
game.scale.setScreenSize(true);
}
window.onload = function() {
// Create game canvas and run some blocks
game = new Phaser.Game(
bSize.bWidth, //is the width of your canvas i guess
bSize.bHeight, //is the height of your canvas i guess
Phaser.AUTO,
'frame', { create: create });
canvas.style.position = "fixed";
canvas.style.left = 0;
canvas.style.top = 0;
}
Since v3.16.0, use the Scale Manager. For short:
var config = {
type: Phaser.AUTO,
scale: {
mode: Phaser.Scale.FIT,
parent: 'phaser-example',
autoCenter: Phaser.Scale.CENTER_BOTH,
width: 800,
height: 600
},
//... other settings
scene: GameScene
};
var game = new Phaser.Game(config);
Here is the full code and here are some useful examples using the Scale Manager.
There isn't a scale manager for Phaser 3 yet but it's in development. For now I suggest following this tutorial. It basically centres the canvas with some CSS, then calls a resize function that handles maintaining the game ratio when the resize event is emitted by the window.
Here is the code used in the tutorial linked above:
The css:
canvas {
display: block;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
The resize function:
function resize() {
var canvas = document.querySelector("canvas");
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var windowRatio = windowWidth / windowHeight;
var gameRatio = game.config.width / game.config.height;
if(windowRatio < gameRatio){
canvas.style.width = windowWidth + "px";
canvas.style.height = (windowWidth / gameRatio) + "px";
}
else {
canvas.style.width = (windowHeight * gameRatio) + "px";
canvas.style.height = windowHeight + "px";
}
}
Then:
window.onload = function() {
//Game config here
var config = {...};
var game = new Phaser.Game(config);
resize();
window.addEventListener("resize", resize, false);
}
Try adding max-width to canvas css and then max-height based on aspect ratio. E.g:
canvas {
display: block;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 100%;
max-height: 50vw;
}
Scale manager will handle assets(Images,Sprites,..,...) positions and size sometime?

Fabricjs Event stretch vs scale

Need to check if user modified image using scale (controls on corners which resizes object proportionally) or stretching (using controls top center/bottom/left/right)
canvas[index].on('object:modified', function (options) {
...
How can i check this?
You can check using scaleX and scaleY of object. If it is corners than both scaleX and scaleY will change, if left/right only scaleX and for top/bottom scaleY will change.
DEMO
var canvas = new fabric.Canvas('canvas');
canvas.add(new fabric.Circle({
radius:50,
left:100,
top:100
}));
canvas.on('mouse:down',onMouseDown);
canvas.on('mouse:move',onMouseMove);
canvas.on('mouse:up',onMouseUp);
var mouseDown, target, originalState = {};
function onMouseDown(options){
var object = options.target;
if(!object) return;
mouseDown = true;
target = object;
originalState.scaleX = object.scaleX;
originalState.scaleY = object.scaleY;
}
function onMouseMove(options){
if(!(mouseDown || target)) return;
if(originalState.scaleX != target.scaleX && originalState.scaleY != target.scaleY){
console.log('scale using corners');
}
else if(originalState.scaleX != target.scaleX){
console.log('scale using left and right');
}
else if(originalState.scaleY != target.scaleY){
console.log('scale using top and bottom');
}
}
function onMouseUp(options){
mouseDown = false;
originalState = {};
target = null;
}
canvas{
border : 2px solid #000;
}
<script src="https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id='canvas' width=400 height=400></canvas>

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;

Resources