I am using Fabric.js with React/Redux and i am trying to achieve the following effect on canvas: (note i have 2 rulers on the top and left which shouldn't move around when panning, just horizontally and vertically).
This is a video of the effect I am trying to get: https://vid.me/qs5Q
I have the following code:
var canvas = new fabric.Canvas('c');
var topRuler = new fabric.Text('this is the top ruler', {
top: 0,
left: 60,
selectable: false
});
var leftRuler = new fabric.Text('this is the left ruler', {
top: 150,
left: -125,
selectable: false
});
var circle = new fabric.Circle({
radius: 20,
fill: 'green',
left: 180,
top: 100
});
var triangle = new fabric.Triangle({
fill: 'green',
left: 150,
top: 170
});
var panning = false;
var panningOn = false;
leftRuler.setAngle(-90).setCoords();
canvas.add(circle, triangle, topRuler, leftRuler);
canvas.renderAll();
canvas.on('mouse:up', function(e) {
if (panningOn) panning = false;
});
canvas.on('mouse:down', function(e) {
if (panningOn) panning = true;
});
canvas.on('mouse:move', function(e) {
if (panning && e && e.e) {
moveX = e.e.movementX;
moveY = e.e.movementY;
topRuler.set({
top: topRuler.top - moveY,
left: topRuler.left - moveX
});
leftRuler.set({
top: leftRuler.top - moveY,
left: leftRuler.left - moveX
});
canvas.relativePan(new fabric.Point(moveX, moveY));
}
});
$('#toggle').click(function() {
panningOn = !panningOn;
canvas.selection = !panningOn;
canvas.defaultCursor = 'default';
$('#toggle').html('Turn Panning ON');
if (panningOn) {
$('#toggle').html('Turn Panning OFF');
canvas.defaultCursor = 'move';
}
});
/* ZOOM FUNCTIONS */
$('#zoomIn').click(function(){
canvas.setZoom(canvas.getZoom() * 1.1 ) ;
});
$('#zoomOut').click(function(){
canvas.setZoom(canvas.getZoom() / 1.1 ) ;
});
This is a working JSFiddle: https://jsfiddle.net/z3p8utmc/19/
The code is not complete since I do not know exactly how to position the rulers, tried to play with viewPort, canvas size, etc. but did not manage to get the same effect.
Thank you for the help.
Here's a screen shot of the results:
Here's the two zoom functions:
/* ZOOM FUNCTIONS */
$('#zoomIn').click(function() {
rectangle.set({
width: rectangle.width * 1.1,
height: rectangle.height * 1.1
});
rectangle.setCoords();
textSample.set({
top: 60 + (textSample.top - 60) * 1.1,
left: 60 + (textSample.left - 60) * 1.1,
scaleX: textSample.scaleX * 1.1,
scaleY: textSample.scaleY * 1.1
});
textSample.setCoords();
setPins();
addLeftRuler();
addTopRuler();
resizeCanvas();
});
$('#zoomOut').click(function() {
rectangle.set({
width: rectangle.width / 1.1,
height: rectangle.height / 1.1
});
rectangle.setCoords();
textSample.set({
top: 60 + (textSample.top - 60) / 1.1,
left: 60 + (textSample.left - 60) / 1.1,
scaleX: textSample.scaleX / 1.1,
scaleY: textSample.scaleY / 1.1
})
textSample.setCoords();
setPins();
addLeftRuler();
addTopRuler();
resizeCanvas();
});
Panning is done using a 3rd-Party control, and the traditional way, since that's what the video shows (and you wanted) - top and left rulers are kept in place by updating their positions as scrolling occurs.
Zooming is done by scaling the individual objects based on point (60, 60) - as in the video. Pins are reset, rulers are rescaled, and canvas is resized - as needed.
The trick is calculating and recalculating everything as user actions are taken and events are firing.
Hopefully this gets you the canvas foundation you need to build upon.
Here's a working JSFiddle, https://jsfiddle.net/rekrah/hs6jL8d4/.
Related
I am having problem trying to make a spinning wheel spin automatically on page load and also display multiple wheels on the same page. The spinner is based on Phaser. I am not a JavaScript nor Phaser programmer so your assistance is appreciated. This is probably something simple to do.
Here is the game.js code for the spinning wheel. (The complete spinning wheel script can be downloaded here)
// the game itself
var game;
var gameOptions = {
// slices (prizes) placed in the wheel
slices: 8,
// prize names, starting from 12 o'clock going clockwise
slicePrizes: ["A KEY!!!", "50 STARS", "500 STARS", "BAD LUCK!!!", "200 STARS", "100 STARS", "150 STARS", "BAD LUCK!!!"],
// wheel rotation duration, in milliseconds
rotationTime: 3000
}
// once the window loads...
window.onload = function() {
// game configuration object
var gameConfig = {
// render type
type: Phaser.CANVAS,
// game width, in pixels
width: 550,
// game height, in pixels
height: 550,
// game background color
backgroundColor: 0x880044,
// scenes used by the game
scene: [playGame]
};
// game constructor
game = new Phaser.Game(gameConfig);
// pure javascript to give focus to the page/frame and scale the game
window.focus()
resize();
window.addEventListener("resize", resize, false);
}
// PlayGame scene
class playGame extends Phaser.Scene{
// constructor
constructor(){
super("PlayGame");
}
// method to be executed when the scene preloads
preload(){
// loading assets
this.load.image("wheel", "wheel.png");
this.load.image("pin", "pin.png");
}
// method to be executed once the scene has been created
create(){
// adding the wheel in the middle of the canvas
this.wheel = this.add.sprite(game.config.width / 2, game.config.height / 2, "wheel");
// adding the pin in the middle of the canvas
this.pin = this.add.sprite(game.config.width / 2, game.config.height / 2, "pin");
// adding the text field
this.prizeText = this.add.text(game.config.width / 2, game.config.height - 20, "Spin the wheel", {
font: "bold 32px Arial",
align: "center",
color: "white"
});
// center the text
this.prizeText.setOrigin(0.5);
// the game has just started = we can spin the wheel
this.canSpin = true;
// waiting for your input, then calling "spinWheel" function
this.input.on("pointerdown", this.spinWheel, this);
}
// function to spin the wheel
spinWheel(){
// can we spin the wheel?
if(this.canSpin){
// resetting text field
this.prizeText.setText("");
// the wheel will spin round from 2 to 4 times. This is just coreography
var rounds = Phaser.Math.Between(2, 4);
// then will rotate by a random number from 0 to 360 degrees. This is the actual spin
var degrees = Phaser.Math.Between(0, 360);
// before the wheel ends spinning, we already know the prize according to "degrees" rotation and the number of slices
var prize = gameOptions.slices - 1 - Math.floor(degrees / (360 / gameOptions.slices));
// now the wheel cannot spin because it's already spinning
this.canSpin = false;
// animation tweeen for the spin: duration 3s, will rotate by (360 * rounds + degrees) degrees
// the quadratic easing will simulate friction
this.tweens.add({
// adding the wheel to tween targets
targets: [this.wheel],
// angle destination
angle: 360 * rounds + degrees,
// tween duration
duration: gameOptions.rotationTime,
// tween easing
ease: "Cubic.easeOut",
// callback scope
callbackScope: this,
// function to be executed once the tween has been completed
onComplete: function(tween){
// displaying prize text
this.prizeText.setText(gameOptions.slicePrizes[prize]);
// player can spin again
this.canSpin = true;
}
});
}
}
}
// pure javascript to scale the game
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";
}
}
Does anyone know how to make it so that
it will start spinning as soon as the page is loaded.
place more than one wheel on a page. When I call the JavaScript twice, it does not display two wheels. The purpose of this exercise is to feed the wheel with two different "degrees" so that it can display the two results. The alternative will be to spin the wheel twice. After the first spin, display the first result and spin again to display the second result. But this is probably harder to do than simply display two wheels.
Thanks in advance.
For the first part of your question , '...spinning as soon as page is loaded..', the answer is:
just add the code line this.spinWheel(); at the end of the create function, this will start it right away.
For the second part:
just add in the gameConfig - object the property parent, with an id of an html-tag
create a game instance (no action needed)
than copy and past the whole gameConfig
change the parent, to a different tag-id
create a second game instance. ( add the line game = new Phaser.Game(gameConfig);)
Here a short Demo showcasing, these two fixes:
(depending on your exact goal, and what you want to achieve, there might be better ways to solve this)
FYI:
Images are not loaded, because they won't work here, but one can see how the auto spinning would work.
I remove code that was not so relevant for the demo, so that it can be understund more easy.
document.body.style = 'margin:0;';
var gameOptions = {
slices: 8,
slicePrizes: ["A KEY!!!", "50 STARS", "500 STARS", "BAD LUCK!!!", "200 STARS", "100 STARS", "150 STARS", "BAD LUCK!!!"],
rotationTime: 3000
}
class playGame extends Phaser.Scene{
constructor(){
super("PlayGame");
}
preload(){
//this.load.image("wheel", "wheel.png");
//this.load.image("pin", "pin.png");
}
create(){
this.wheel = this.add.sprite(game.config.width / 2, game.config.height / 2, "wheel");
this.pin = this.add.sprite(game.config.width / 2, game.config.height / 2, "pin");
this.prizeText = this.add.text(game.config.width / 2, game.config.height - 20, "Spin the wheel", {
font: "bold 32px Arial",
align: "center",
color: "white"
});
this.prizeText.setOrigin(0.5);
this.canSpin = true;
this.input.on("pointerdown", this.spinWheel, this);
this.spinWheel();
}
spinWheel(){
if(this.canSpin){
this.prizeText.setText("");
var rounds = Phaser.Math.Between(2, 4);
var degrees = Phaser.Math.Between(0, 360);
var prize = gameOptions.slices - 1 - Math.floor(degrees / (360 / gameOptions.slices));
this.canSpin = false;
this.tweens.add({
targets: [this.wheel],
angle: 360 * rounds + degrees,
duration: gameOptions.rotationTime,
ease: "Cubic.easeOut",
callbackScope: this,
onComplete: function(tween){
this.prizeText.setText(gameOptions.slicePrizes[prize]);
this.canSpin = true;
}
});
}
}
}
var gameConfig = {
type: Phaser.CANVAS,
parent: 'wheel1',
width: 200,
height: 200,
backgroundColor: 0x880044,
scene: [playGame]
};
var game = new Phaser.Game(gameConfig);
gameConfig = {
type: Phaser.CANVAS,
parent: 'wheel2',
width: 200,
height: 200,
// I altered also the background color to show the difference
backgroundColor: 0x0ff044,
scene: [playGame]
};
game = new Phaser.Game(gameConfig);
#wheel1, #wheel2, canvas{
display:inline-block;
padding:0;
margin:0;
}
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
<div id="wheel1"></div><div id="wheel2"></div>
Updated - Update:
The main problem is that I made a mistake, the property is not parentElement it is parent. I tried to fix this in following demo.
// the game itself
var game;
var gameOptions = {
// slices (prizes) placed in the wheel
slices: 8,
// prize names, starting from 12 o'clock going clockwise
// this array has to contain atleast 8 value
slicePrizes: ["A KEY!!!", "50 STARS", "500 STARS", "BAD LUCK!!!", "200 STARS", "100 STARS", "150 STARS", "BAD LUCK!!!"],
// wheel rotation duration, in milliseconds
rotationTime: 3000
}
var gameConfig;
// once the window loads...
window.onload = function () {
gameConfig = {
type: Phaser.CANVAS,
parent: 'wheel1',
width: 200,
height: 200,
backgroundColor: 0x880044,
scene: [playGame]
};
var game = new Phaser.Game(gameConfig);
game.scene.start('PlayGame', { degrees: 60 });
gameConfig = {
type: Phaser.CANVAS,
parent: 'wheel2',
width: 200,
height: 200,
// I altered also the background color to show the difference
backgroundColor: 0x0ff044,
scene: [playGame]
};
var game2 = new Phaser.Game(gameConfig);
game2.scene.start('PlayGame', { degrees: 40 });
window.focus()
resize();
window.addEventListener("resize", resize, false);
}
// PlayGame scene
class playGame extends Phaser.Scene {
// constructor
constructor() {
super({ key: "PlayGame" });
}
// method to be executed when the scene preloads
preload() {
// loading assets
//this.load.image("wheel", "wheel.png");
//this.load.image("pin", "pin.png");
}
// method to be executed once the scene has been created
create(data) {
// adding the wheel in the middle of the canvas
this.wheel = this.add.sprite(gameConfig.width / 2, gameConfig.height / 2, "wheel");
// adding the pin in the middle of the canvas
this.pin = this.add.sprite(gameConfig.width / 2, gameConfig.height / 2, "pin");
// adding the text field
this.prizeText = this.add.text(gameConfig.width / 2, gameConfig.height - 20, "Spin the wheel", {
font: "bold 32px Arial",
align: "center",
color: "black"
});
// center the text
this.prizeText.setOrigin(0.5);
// the game has just started = we can spin the wheel
this.canSpin = true;
//this.input.on("pointerdown", this.spinWheel, this);
this.spinWheel(data.degrees);
}
// function to spin the wheel
spinWheel(degrees) {
// can we spin the wheel?
if (this.canSpin) {
// resetting text field
this.prizeText.setText("");
// the wheel will spin round from 2 to 4 times. This is just coreography
var rounds = Phaser.Math.Between(8, 10);
//var degrees = Phaser.Math.Between(0, 360);
var prize = gameOptions.slices - 1 - Math.floor(degrees / (360 / gameOptions.slices));
// now the wheel cannot spin because it's already spinning
this.canSpin = false;
// animation tweeen for the spin: duration 3s, will rotate by (360 * rounds + degrees) degrees
// the quadratic easing will simulate friction
this.tweens.add({
// adding the wheel to tween targets
targets: [this.wheel],
// angle destination
angle: 360 * rounds + degrees,
// tween duration
duration: gameOptions.rotationTime,
// tween easing
ease: "Cubic.easeOut",
// callback scope
callbackScope: this,
// function to be executed once the tween has been completed
onComplete: function (tween) {
// displaying prize text
this.prizeText.setText(gameOptions.slicePrizes[prize]);
// player can spin again
this.canSpin = true;
}
});
}
}
}
// pure javascript to scale the game
function resize() {
var canvas = document.querySelector("#wheels");
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var windowRatio = windowWidth / windowHeight;
var gameRatio = gameConfig.width / gameConfig.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";
}
}
body{
padding: 0px;
margin: 0px;
}
#wheels{
display: flex;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
align-items: stretch;
}
#wheel1, #wheel2, canvas{
flex-grow: 1;
margin: 0;
padding: 0;
}
canvas{
width: 100%;
}
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
<div id="wheels">
<div id="wheel1"></div>
<div id="wheel2"></div>
</div>
I can zoom-in and zoom-out using a computer inside the canvas. However, I cannot zoom-in and zoom-out on mobile devices (smartphone, tablet, etc.). When I looked at the documentation, I found the "touch: gesture" method, but I could not adapt it to my code and execute it. How can I do that? Below I add the code I use.
var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage('https://i.hizliresim.com/iBHC0t.jpg', canvas.renderAll.bind(canvas));
canvas.selection = false;
//var uniqid = "0";
var uniqids = 0;
$("#door").on("click", function(e) {
rect = new fabric.Rect({
id:uniqid,
left: 40,
top: 40,
width: 35,
height: 50,
fill: 'blue',
stroke: 'blue',
strokeWidth: 5,
strokeUniform: false,
hasControls : true,
});
var uniqid = uniqids.toString();
var text = new fabric.Text(uniqid, {
fontSize: 30,
originX: 'center',
originY: 'right'
});
var group = new fabric.Group([ rect, text ], {
left: 0,
top: 100,
});
canvas.add(group);
canvas.add(rect);
uniqids++;
});
//*****************************
canvas.on('mouse:wheel', function(opt) {
var delta = opt.e.deltaY;
var zoom = canvas.getZoom();
zoom *= 0.999 ** delta;
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
canvas.setZoom(zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
})
//***************************************
$("#save").on("click", function(e) {
$(".save").html(canvas.toSVG());
});
$('#delete').click(function() {
var activeObject = canvas.getActiveObjects();
canvas.discardActiveObject();
canvas.remove(...activeObject);
});
$("#btnResetZoom").on("click", function(e) {
canvas.setViewportTransform([1,0,0,1,0,0]);
});
canvas.on('mouse:wheel', function(opt) {
var delta = opt.e.deltaY;
var zoom = canvas.getZoom();
zoom *= 0.999 ** delta;
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
});
var shiftKeyDown = true;
var mouseDownPoint = null;
canvas.on('mouse:move', function(options) {
if (shiftKeyDown && mouseDownPoint) {
var pointer = canvas.getPointer(options.e, true);
var mouseMovePoint = new fabric.Point(pointer.x, pointer.y);
canvas.relativePan(mouseMovePoint.subtract(mouseDownPoint));
mouseDownPoint = mouseMovePoint;
keepPositionInBounds(canvas);
}
});
/*
canvas.on('mouse:over', function(e) {
e.target.set('fill', 'red');
canvas.renderAll();
});
*/
#c {
background-color: grey;
margin-top: 10px;
}
button {
padding: 10px 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.1.0/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<button id="door">Door</button>
<button id="delete">Delete Door</button>
<button id="save">Save</button>
<button id="btnResetZoom">Reset Zoom</i></button>
<p>Save bastıktan sonra altta SVG dosyası oluşur</p>
<br>
<canvas id="c" width="800" height="800"></canvas>
<br>
<p class="save">
</p>
Thank you for your question. I encountered the same issue once and I figured it out very easily.
However, I cannot zoom-in and zoom-out on mobile devices (smartphone, tablet, etc.).
Did you run Demo for touch events on your smartphone or tablet?
Then as you can see, there's no need to add some custom code for the pinch zoom or rotate. It automatically enables you to use those features.
The main point is, you should use the custom version of Fabric with touch events enabled.
It is just fabric_with_gestures.js. I also used prism.js together.
Fabric JS question.
SCREENSHOT this is what my Fabric JS app looks like
CODEPEN https://codepen.io/zaynbuksh/pen/VVKRxj?editors=0011 (alt-click-drag to pan, scroll-wheel to zoom)
TLDR; How do I get the line to stick after panning and zooming a few times?
I am developing a Fabric JS app for labeling images of specimens. As part of this, people want to be able to zoom in on what each label is pointing at. I have been asked to make the labels remain visible when the specimen image is zoomed in. From research, people recommend two canvases stacked on top of each other.
I have created two Fabric JS canvas instances, layered on top of each other. The canvas at the bottom holds a background image that can be zoomed and panned, the canvas above it shows a pointer-line/label that is not zoomed (to keep the label visible at all times).
At first everything works - the line stays in sync with the image when I pan and zoom the first time only. I get problems with syncing the line to the image after that.
The problem manifests when I pan then zoom two or more times. The problem repeats each time I pan and then zoom i.e. the line moves when I zoom, but then stays in sync when I pan, moves again when I zoom again, pans normally and so on...
(Pan is handled by alt-click-drag, Zoom is handled by scroll wheel)
/*
"mouse:wheel" event is where zooms are handled
"mouse:move" event is where panning is handled
*/
// create Fabric JS canvas'
var labelsCanvas = new fabric.Canvas("labelsCanvas");
var specimenCanvas = new fabric.Canvas("specimenCanvas");
//set defaults
var startingPositionForLine = 100;
const noZoom = 1;
var wasPanned = false;
var panY2 = startingPositionForLine;
var panX2 = startingPositionForLine;
var zoomY2 = startingPositionForLine;
var zoomX2 = startingPositionForLine;
// set starting zoom for specimen canvas
var specimenZoom = noZoom;
/*
Add pointer, label and background image into canvas
*/
// create a pointer line
var line = new fabric.Line([150, 35, panX2, panY2], {
fill: "red",
stroke: "red",
strokeWidth: 3,
strokeDashArray: [5, 2],
// selectable: false,
evented: false
});
// create text label
var text = new fabric.Text("Label 1", {
left: 100,
top: 0,
// selectable: false,
evented: false,
backgroundColor: "red"
});
// add both into "Labels" canvas
labelsCanvas.add(text);
labelsCanvas.add(line);
// add a background image into Specimen canvas
fabric.Image.fromURL(
"https://upload.wikimedia.org/wikipedia/commons/c/cb/Skull_brain_human_normal.svg",
function(oImg) {
oImg.left = 0;
oImg.top = 0;
oImg.scaleToWidth(300);
oImg.scaleToHeight(300);
specimenCanvas.add(oImg);
}
);
/*
Handle mouse events
*/
// zoom the specimen image canvas via a mouse scroll-wheel event
labelsCanvas.on("mouse:wheel", function(opt) {
// scroll value e.g. 5, 6 -1, -18
var delta = opt.e.deltaY;
// zoom level in specimen
var zoom = specimenCanvas.getZoom();
console.log("zoom ", zoom);
// make zoom smaller
zoom = zoom + delta / 200;
// use sane defaults for zoom
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
// create new zoom value
zoomX2 = panX2 * zoom;
zoomY2 = panY2 * zoom;
// save the zoom
specimenZoom = zoom;
// set the specimen canvas zoom
specimenCanvas.setZoom(zoom);
// move line to sync it with the zoomed image
line.set({
x2: zoomX2,
y2: zoomY2
});
console.log("zoomed line ", line.x2);
// render the changes
this.requestRenderAll();
// block default mouse behaviour
opt.e.preventDefault();
opt.e.stopPropagation();
console.log(labelsCanvas.viewportTransform[4]);
// stuff I've tried to fix errors
line.setCoords();
specimenCanvas.calcOffset();
});
// pan the canvas
labelsCanvas.on("mouse:move", function(opt) {
if (this.isDragging) {
// pick up the click and drag event
var e = opt.e;
// sync the label position with the panning
text.left = text.left + (e.clientX - this.lastPosX);
var x2ToUse;
var y2ToUse;
// UNZOOMED canvas is being panned
if (specimenZoom === noZoom) {
x2ToUse = panX2;
y2ToUse = panY2;
// move the image using the difference between
// the current position and last known position
line.set({
x1: line.x1 + (e.clientX - this.lastPosX),
y1: line.y1,
x2: x2ToUse + (e.clientX - this.lastPosX),
y2: y2ToUse + (e.clientY - this.lastPosY)
});
// set the new panning value
panX2 = line.x2;
panY2 = line.y2;
// stuff I've tried
// zoomX2 = line.x2;
// zoomY2 = line.y2;
}
// ZOOMED canvas is being panned
else
{
x2ToUse = zoomX2;
y2ToUse = zoomY2;
// stuff I've tried
// x2ToUse = panX2;
// y2ToUse = panY2;
// move the image using the difference between
// the current position and last known ZOOMED position
line.set({
x1: line.x1 + (e.clientX - this.lastPosX),
y1: line.y1,
x2: x2ToUse + (e.clientX - this.lastPosX),
y2: y2ToUse + (e.clientY - this.lastPosY)
});
zoomX2 = line.x2;
zoomY2 = line.y2;
}
// hide label/pointer when it is out of view
if (text.left < 0 || line.y2 < 35) {
text.animate("opacity", "0", {
duration: 15,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
line.animate("opacity", "0", {
duration: 15,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
}
// show label/pointer when it is in view
else
{
text.animate("opacity", "1", {
duration: 25,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
line.animate("opacity", "1", {
duration: 25,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
}
specimenCanvas.viewportTransform[4] += e.clientX - this.lastPosX;
specimenCanvas.viewportTransform[5] += e.clientY - this.lastPosY;
this.requestRenderAll();
specimenCanvas.requestRenderAll();
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
}
console.log(line.x2);
wasPanned = true;
});
labelsCanvas.on("mouse:down", function(opt) {
var evt = opt.e;
if (evt.altKey === true) {
this.isDragging = true;
this.selection = false;
this.lastPosX = evt.clientX;
this.lastPosY = evt.clientY;
}
});
labelsCanvas.on("mouse:up", function(opt) {
this.isDragging = false;
this.selection = true;
});
.canvas-container {
position: absolute!important;
left: 0!important;
top: 0!important;
}
.canvas {
position: absolute;
top: 0;
right: 0;
border: solid red 1px;
}
.label-canvas {
z-index: 2;
}
.specimen-canvas {
z-index: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.js"></script>
<h1>
Dual canvas test
</h1>
<div style="position: relative; height: 300px">
<canvas class="canvas specimen-canvas" id="specimenCanvas" width="300" height="300"></canvas>
<canvas class="canvas label-canvas" id="labelsCanvas" width="300" height="300"></canvas>
</div>
As a side note, I think you're overcomplicating things a little bit. You don't really need to store panX/panY and zoomX/zoomY (zoomed pan as I've guessed) separately, they're already there in your line's coordinates. Just saying, because they've probably contributed to the confusion/debugging. The core idea of a fix, however, is that you should multiply your line's coordinates not by the whole zoom value but by the newZoom / previousZoom ratio. I've updated your snippet, it seems to work as expected:
/*
"mouse:wheel" event is where zooms are handled
"mouse:move" event is where panning is handled
*/
// create Fabric JS canvas'
var labelsCanvas = new fabric.Canvas("labelsCanvas");
var specimenCanvas = new fabric.Canvas("specimenCanvas");
//set defaults
var startingPositionForLine = 100;
const noZoom = 1;
var wasPanned = false;
var panY2 = startingPositionForLine;
var panX2 = startingPositionForLine;
var zoomY2 = startingPositionForLine;
var zoomX2 = startingPositionForLine;
// set starting zoom for specimen canvas
var specimenZoom = noZoom;
var prevZoom = noZoom;
/*
Add pointer, label and background image into canvas
*/
// create a pointer line
var line = new fabric.Line([150, 35, panX2, panY2], {
fill: "red",
stroke: "red",
strokeWidth: 3,
strokeDashArray: [5, 2],
// selectable: false,
evented: false
});
// create text label
var text = new fabric.Text("Label 1", {
left: 100,
top: 0,
// selectable: false,
evented: false,
backgroundColor: "red"
});
// add both into "Labels" canvas
labelsCanvas.add(text);
labelsCanvas.add(line);
// add a background image into Specimen canvas
fabric.Image.fromURL(
"https://upload.wikimedia.org/wikipedia/commons/c/cb/Skull_brain_human_normal.svg",
function(oImg) {
oImg.left = 0;
oImg.top = 0;
oImg.scaleToWidth(300);
oImg.scaleToHeight(300);
specimenCanvas.add(oImg);
}
);
window.specimenCanvas = specimenCanvas
/*
Handle mouse events
*/
// zoom the specimen image canvas via a mouse scroll-wheel event
labelsCanvas.on("mouse:wheel", function(opt) {
// scroll value e.g. 5, 6 -1, -18
var delta = opt.e.deltaY;
// zoom level in specimen
var zoom = specimenCanvas.getZoom();
var lastZoom = zoom
// make zoom smaller
zoom = zoom + delta / 200;
// use sane defaults for zoom
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
// save the zoom
specimenZoom = zoom;
// set the specimen canvas zoom
specimenCanvas.setZoom(zoom);
// move line to sync it with the zoomed image
var zoomRatio = zoom / lastZoom
console.log('zoom ratio: ', zoomRatio)
line.set({
x2: line.x2 * zoomRatio,
y2: line.y2 * zoomRatio
});
// console.log("zoomed line ", line.x2);
// render the changes
this.requestRenderAll();
// block default mouse behaviour
opt.e.preventDefault();
opt.e.stopPropagation();
// console.log(labelsCanvas.viewportTransform[4]);
// stuff I've tried to fix errors
line.setCoords();
specimenCanvas.calcOffset();
});
// pan the canvas
labelsCanvas.on("mouse:move", function(opt) {
if (this.isDragging) {
// pick up the click and drag event
var e = opt.e;
// sync the label position with the panning
text.left = text.left + (e.clientX - this.lastPosX);
// UNZOOMED canvas is being panned
if (specimenZoom === noZoom) {
x2ToUse = panX2;
y2ToUse = panY2;
// move the image using the difference between
// the current position and last known position
line.set({
x1: line.x1 + (e.clientX - this.lastPosX),
y1: line.y1,
x2: line.x2 + (e.clientX - this.lastPosX),
y2: line.y2 + (e.clientY - this.lastPosY)
});
// stuff I've tried
// zoomX2 = line.x2;
// zoomY2 = line.y2;
}
// ZOOMED canvas is being panned
else
{
// move the image using the difference between
// the current position and last known ZOOMED position
line.set({
x1: line.x1 + (e.clientX - this.lastPosX),
y1: line.y1,
x2: line.x2 + (e.clientX - this.lastPosX),
y2: line.y2 + (e.clientY - this.lastPosY)
});
}
// hide label/pointer when it is out of view
if (text.left < 0 || line.y2 < 35) {
text.animate("opacity", "0", {
duration: 15,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
line.animate("opacity", "0", {
duration: 15,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
}
// show label/pointer when it is in view
else
{
text.animate("opacity", "1", {
duration: 25,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
line.animate("opacity", "1", {
duration: 25,
onChange: labelsCanvas.renderAll.bind(labelsCanvas)
});
}
specimenCanvas.viewportTransform[4] += e.clientX - this.lastPosX;
specimenCanvas.viewportTransform[5] += e.clientY - this.lastPosY;
this.requestRenderAll();
specimenCanvas.requestRenderAll();
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
prevZoom = specimenCanvas.getZoom()
}
// console.log(line.x2);
wasPanned = true;
});
labelsCanvas.on("mouse:down", function(opt) {
var evt = opt.e;
if (evt.altKey === true) {
this.isDragging = true;
this.selection = false;
this.lastPosX = evt.clientX;
this.lastPosY = evt.clientY;
}
});
labelsCanvas.on("mouse:up", function(opt) {
this.isDragging = false;
this.selection = true;
});
.canvas-container {
position: absolute!important;
left: 0!important;
top: 0!important;
}
.canvas {
position: absolute;
top: 0;
right: 0;
border: solid red 1px;
}
.label-canvas {
z-index: 2;
}
.specimen-canvas {
z-index: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.js"></script>
<h1>
Dual canvas test
</h1>
<div style="position: relative; height: 300px">
<canvas class="canvas specimen-canvas" id="specimenCanvas" width="300" height="300"></canvas>
<canvas class="canvas label-canvas" id="labelsCanvas" width="300" height="300"></canvas>
</div>
My fiddle: http://jsfiddle.net/yeftc60v/1/
So I'm trying to get the obj.left to be based on the left side(wall) of the group instead of the center. So right now, when I try to get the left of an object, if it's on the left side, it will return a negative number.
I've tried both setOriginY and setOriginX but they don't have any effect.
I'm working around this by doing some maths based on the group's width.
Right now if you hit the "clone" button, the left values read negative values (-100.5 and -0.5). Instead they should be 0 and 99.5(I think), respectively.
Unfortunately, any active group's origin x and y will always be at center. This won't take originX and originY property into consideration, hence setting setOriginX and setOriginY won't have any effect.
To get around this, you need to manually calculate the object's top and left position respective to the selected group, like so ...
let objLeft = obj.left + (active.width / 2);
let objTop = obj.top + (active.height / 2);
ᴅᴇᴍᴏ
var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect({
left: 50,
top: 50,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 1,
opacity: .8
});
var rect1 = new fabric.Rect({
left: 150,
top: 150,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 1,
opacity: .8
});
canvas.add(rect);
canvas.add(rect1);
function clone() {
let active = canvas.getActiveGroup();
if (active) {
active.forEachObject(obj => {
let clone = obj.clone();
// get object's left & top relative to the group
let objLeft = obj.left + (active.width / 2);
let objTop = obj.top + (active.height / 2);
clone.setLeft(objLeft + active.left);
clone.setTop(objTop + active.top);
canvas.add(clone);
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.11/fabric.min.js"></script>
<canvas id="c" width="300" height="300" style="border: 1px solid black;"></canvas>
<button onclick="clone()">
Clone
</button>
I have designed a ruler using fabric.js and when the user mouses over the specific part of the ruler I want to print to the screen the X-coordinate of the ruler (i.e. not the screen coordinate). Right now the ruler canvas starts at position 37 and ends at 726 relative to the ruler, but the ruler goes from 1 to 4600 (it will always start at 1 but the length of the ruler can change). How to transform the mouse coordinates to accurately reflect it's position on the ruler? Here is the code:
var canvas = new fabric.Canvas('canvas');
line_length = input_len;
adjusted_length = (line_length / 666) * 100;
canvas.add(new fabric.Line([0, 0, adjusted_length, 0], {
left: 30,
top: 0,
stroke: '#d89300',
strokeWidth: 3
}));
$('#canvas_container').css('overflow-x', 'scroll');
$('#canvas_container').css('overflow-y', 'hidden');
drawRuler();
function drawRuler() {
$("#ruler[data-items]").val(line_length / 200);
$("#ruler[data-items]").each(function () {
var ruler = $(this).empty(),
len = Number($("#ruler[data-items]").val()) || 0,
item = $(document.createElement("li")),
i;
ruler.append(item.clone().text(1));
for (i = 1; i < len; i++) {
ruler.append(item.clone().text(i * 200));
}
ruler.append(item.clone().text(i * 200));
});
}
canvas.add(new fabric.Text('X-cord', {
fontStyle: 'italic',
fontFamily: 'Hoefler Text',
fontSize: 12,
left: 0,
top: 0,
hasControls: false,
selectable: false
}));
canvas.on('mouse:move', function (options) {
getMouse(options);
});
function getMouse(options) {
canvas.getObjects('text')[0].text =
"X-coords: " + options.e.clientX ; //+ " Y: " + options.e.clientY;
canvas.renderAll();
}
Use the getPointer merthod on canvas Instance.
In your case it should be canvas.getPointer(options.e)which returns an object with x and y properties which represent pointer coordinates relative to canvas.