Fabric JS: sync position of object after pan/zoom across two instances - javascript

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>

Related

I have a map that can be zoomed in and out, how can I easily place markers with javascript?

Its basicly an image, but I want to add some points with dropdowns, and its like 15 points, ajusting it with px would be very time consuming, I wonder if there is any other way around it. Thanks
<div id="map">
<div id="slide">
<img id="mapimg" src="https://i.imgur.com/NLS1KX0.jpg">
<div id="point">
<i id="pointIcon" class="fa-solid fa-location-dot"></i>
</div>
</div>
</div>
$(document).ready(function (){
var scroll_zoom = new ScrollZoom($('#map'),5,0.5)
})
//The parameters are:
//
//container: The wrapper of the element to be zoomed. The script will look for the first child of the container and apply the transforms to it.
//max_scale: The maximum scale (4 = 400% zoom)
//factor: The zoom-speed (1 = +100% zoom per mouse wheel tick)
function ScrollZoom(container,max_scale,factor){
var target = container.children().first()
var size = {w:target.width(),h:target.height()}
var pos = {x:0,y:0}
var scale = 1
var zoom_target = {x:0,y:0}
var zoom_point = {x:0,y:0}
var curr_tranform = target.css('transition')
var last_mouse_position = { x:0, y:0 }
var drag_started = 0
target.css('transform-origin','0 0')
target.on("mousewheel DOMMouseScroll",scrolled)
target.on('mousemove', moved)
target.on('mousedown', function() {
drag_started = 1;
target.css({'cursor':'move', 'transition': 'transform 0s'});
/* Save mouse position */
last_mouse_position = { x: event.pageX, y: event.pageY};
});
target.on('mouseup mouseout', function() {
drag_started = 0;
target.css({'cursor':'default', 'transition': curr_tranform});
});
function scrolled(e){
var offset = container.offset()
zoom_point.x = e.pageX - offset.left
zoom_point.y = e.pageY - offset.top
e.preventDefault();
var delta = e.delta || e.originalEvent.wheelDelta;
if (delta === undefined) {
//we are on firefox
delta = e.originalEvent.detail;
}
delta = Math.max(-1,Math.min(1,delta)) // cap the delta to [-1,1] for cross browser consistency
// determine the point on where the slide is zoomed in
zoom_target.x = (zoom_point.x - pos.x)/scale
zoom_target.y = (zoom_point.y - pos.y)/scale
// apply zoom
scale += delta * factor * scale
scale = Math.max(1,Math.min(max_scale,scale))
// calculate x and y based on zoom
pos.x = -zoom_target.x * scale + zoom_point.x
pos.y = -zoom_target.y * scale + zoom_point.y
update()
}
function moved(event){
if(drag_started == 1) {
var current_mouse_position = { x: event.pageX, y: event.pageY};
var change_x = current_mouse_position.x - last_mouse_position.x;
var change_y = current_mouse_position.y - last_mouse_position.y;
/* Save mouse position */
last_mouse_position = current_mouse_position;
//Add the position change
pos.x += change_x;
pos.y += change_y;
update()
}
}
function update(){
// Make sure the slide stays in its container area when zooming out
if(pos.x>0)
pos.x = 0
if(pos.x+size.w*scale<size.w)
pos.x = -size.w*(scale-1)
if(pos.y>0)
pos.y = 0
if(pos.y+size.h*scale<size.h)
pos.y = -size.h*(scale-1)
target.css('transform','translate('+(pos.x)+'px,'+(pos.y)+'px) scale('+scale+','+scale+')')
}
}
I tried using this, but the image is very long, and it would take to much time, I wonder if there is any easy way to do it.
#point {
position: relative;
overflow: hidden;
margin-left: 338px;
margin-top: -243px;
width: 25px;
height: 25px;
}
I tried using this, but the image is very long, and it would take to much time, I wonder if there is any easy way to do it.
#point {
position: relative;
overflow: hidden;
margin-left: 338px;
margin-top: -243px;
width: 25px;
height: 25px;
}

How can I zoom in Fabric JS on mobile devices?

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.

KonvaJS / HTML5 canvas infinite looping tilemap. Setting camera position

I'm trying to create an infinite looping canvas based on a main 'grid'. Scaled down fiddle here with the grid in the centre of the viewport.
JS Fiddle here
In the fiddle I have my main grid of coloured squares in the centre, I want them tiled infinitely in all directions. Obviously this isn't realistically possible, so I want to give the illusion of infinite by just redrawing the grids based on the scroll direction.
I found some good articles:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Tilemaps/Square_tilemaps_implementation:_Scrolling_maps
https://gamedev.stackexchange.com/questions/71583/html5-dynamic-canvas-grid-for-scrolling-a-big-map
And the best route seems to be to get the drag direction and then reset camera to that point, so the layers scroll under the main canvas viewport, thus meaning the camera can never reach the edge of the main viewport canvas.
I've worked on adding some event listeners for mouse drags :
Fiddle with mouse events
var bMouseDown = false;
var oPreviousCoords = {
'x': 0,
'y': 0
}
var oDelta;
var oEndCoords;
var newLayerTop;
$(document).on('mousedown', function (oEvent) {
bMouseDown = true;
oPreviousCoords = {
'x': oEvent.pageX,
'y': oEvent.pageY
}
});
$(document).on('mouseup', function (oEvent) {
bMouseDown = false;
oPreviousCoords = {
'x': oEvent.pageX,
'y': oEvent.pageY
}
oEndCoords = oDelta
if(oEndCoords.y < -300){
if(newLayerTop){
newLayerTop.destroy();
}
layerCurentPosition = layer.position();
newLayerTop = layer.clone();
newLayerTop.position({
x: layerCurentPosition.x,
y: layerCurentPosition.y -1960
});
stage.add(newLayerTop)
stage.batchDraw();
}
});
$(document).on('mousemove', function (oEvent) {
if (!bMouseDown) {
return;
}
oDelta = {
'x': oPreviousCoords.x - oEvent.pageX,
'y': oPreviousCoords.y - oEvent.pageY
}
});
But I can't reliably work out the co-ordinates for each direction and then how to reset the camera position.
As you need "infinite" canvas I suggest to not use scrolling and make canvas as large as user viewport. Then you can emulate camera and on every move, you need to draw a new grid on the canvas. You just need to carefully calculate the position of the grid.
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
draggable: true
});
const layer = new Konva.Layer();
stage.add(layer);
const WIDTH = 100;
const HEIGHT = 100;
const grid = [
['red', 'yellow'],
['green', 'blue']
];
function checkShapes() {
const startX = Math.floor((-stage.x() - stage.width()) / WIDTH) * WIDTH;
const endX = Math.floor((-stage.x() + stage.width() * 2) / WIDTH) * WIDTH;
const startY = Math.floor((-stage.y() - stage.height()) / HEIGHT) * HEIGHT;
const endY = Math.floor((-stage.y() + stage.height() * 2) / HEIGHT) * HEIGHT;
for(var x = startX; x < endX; x += WIDTH) {
for(var y = startY; y < endY; y += HEIGHT) {
const indexX = Math.abs(x / WIDTH) % grid.length;
const indexY = Math.abs(y / HEIGHT) % grid[0].length;
layer.add(new Konva.Rect({
x,
y,
width: WIDTH,
height: HEIGHT,
fill: grid[indexX][indexY]
}))
}
}
}
checkShapes();
layer.draw();
stage.on('dragend', () => {
layer.destroyChildren();
checkShapes();
layer.draw();
})
<script src="https://unpkg.com/konva#^2/konva.min.js"></script>
<div id="container"></div>
If you need scrolling you can listen wheel event on stage and move into desired direction.

I use fabricJs and want to get mouse position in zooming

I am using fabricJs library to create rectangles and want to get mouse position after zooming
I use this code to start drawing the rectangle, in scale = 1, it works right but after i get zoom in for example and click i get the rectangle start point in the point not i click on, what cause this ?
// Create new rectangle
canvas.on('mouse:down', function (options) {
if (canvas.getActiveObject()) {
return false;
}
started = true;
ex = (posx);
ey = (posy);
var colors = ['#FF8080', '#D5D5E6', '#C0F2C0', '#8080E4', '#CCCCAA'];
var rectangle_color = colors[Math.floor(Math.random() * colors.length)];
var square = new fabric.Rect({
width: 0,
height: 0,
left: ex,
top: ey,
fill: rectangle_color
});
var square = new fabric.Rect({ width: 0, height: 0, left: ex, top: ey, fill: rectangle_color });
canvas.add(square);
canvas.setActiveObject(square);
});
You need your real coordinates of your mouse not some scope declared posx and posy:
canvas.on('mouse:down', function (event) {
if (canvas.getActiveObject()) {
return false;
}
var pointer = canvas.getPointer(event.e);
var posx = pointer.x;
var posy = pointer.y;
//...
//your code
//...
}

Kineticjs - free rotate on image

I need help only having the anchors for rotating. Right now there is five anchors and I don't know how to get rid of all of them except the rotate one. I would also only like the anchors to show when the user hovers over the image
Here is my code
<html>
<head>
<style>
body {
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<body onmousedown="return false;">
<div id="container"></div>
<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v4.7.4.min.js">
</script>
<script>
function update(activeAnchor) {
var group = activeAnchor.getParent();
var topLeft = group.get('.topLeft')[0];
var topRight = group.get('.topRight')[0];
var bottomRight = group.get('.bottomRight')[0];
var bottomLeft = group.get('.bottomLeft')[0];
var rotateAnchor = group.get('.rotateAnchor')[0];
var image = group.get('Image')[0];
var anchorX = activeAnchor.getX();
var anchorY = activeAnchor.getY();
var imageWidth = image.getWidth();
var imageHeight = image.getHeight();
var offsetX = Math.abs((topLeft.getX() + bottomRight.getX() + 10) / 2);
var offsetY = Math.abs((topLeft.getY() + bottomRight.getY() + 10) / 2);
// update anchor positions
switch (activeAnchor.getName()) {
case 'rotateAnchor':
group.setOffset(offsetX, offsetY);
break;
case 'topLeft':
topRight.setY(anchorY);
bottomLeft.setX(anchorX);
break;
case 'topRight':
topLeft.setY(anchorY);
bottomRight.setX(anchorX);
break;
case 'bottomRight':
topRight.setX(anchorX);
bottomLeft.setY(anchorY);
break;
case 'bottomLeft':
topLeft.setX(anchorX);
bottomRight.setY(anchorY);
break;
}
rotateAnchor.setX(topRight.getX() + 5);
rotateAnchor.setY(topRight.getY() + 20);
image.setPosition((topLeft.getPosition().x + 20), (topLeft.getPosition().y + 20));
var width = topRight.getX() - topLeft.getX() - 30;
var height = bottomLeft.getY() - topLeft.getY() - 30;
if (width && height) {
image.setSize(width, height);
}
}
function addAnchor(group, x, y, name, dragBound) {
var stage = group.getStage();
var layer = group.getLayer();
var anchor = new Kinetic.Circle({
x: x,
y: y,
stroke: '#666',
fill: '#ddd',
strokeWidth: 2,
radius: 8,
name: name,
draggable: true,
dragOnTop: false
});
if (dragBound == 'rotate') {
anchor.setAttrs({
dragBoundFunc: function (pos) {
return getRotatingAnchorBounds(pos, group);
}
});
}
anchor.on('dragmove', function() {
update(this);
layer.draw();
});
anchor.on('mousedown touchstart', function() {
group.setDraggable(false);
this.moveToTop();
});
anchor.on('dragend', function() {
group.setDraggable(true);
layer.draw();
});
// add hover styling
anchor.on('mouseover', function() {
var layer = this.getLayer();
document.body.style.cursor = 'pointer';
this.setStrokeWidth(4);
layer.draw();
});
anchor.on('mouseout', function() {
var layer = this.getLayer();
document.body.style.cursor = 'default';
this.setStrokeWidth(2);
layer.draw();
});
group.add(anchor);
}
function loadImages(sources, callback) {
var images = {};
var loadedImages = 0;
var numImages = 0;
for(var src in sources) {
numImages++;
}
for(var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if(++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
function getRotatingAnchorBounds(pos, group) {
var topLeft = group.get('.topLeft')[0];
var bottomRight = group.get('.bottomRight')[0];
var topRight = group.get('.topRight')[0];
var absCenterX = Math.abs((topLeft.getAbsolutePosition().x + 5 + bottomRight.getAbsolutePosition().x + 5) / 2);
var absCenterY = Math.abs((topLeft.getAbsolutePosition().y + 5 + bottomRight.getAbsolutePosition().y + 5) / 2);
var relCenterX = Math.abs((topLeft.getX() + bottomRight.getX()) / 2);
var relCenterY = Math.abs((topLeft.getY() + bottomRight.getY()) / 2);
var radius = distance(relCenterX, relCenterY, topRight.getX() + 5, topRight.getY() + 20);
var scale = radius / distance(pos.x, pos.y, absCenterX, absCenterY);
var realRotation = Math.round(degrees(angle(relCenterX, relCenterY, topRight.getX() + 5, topRight.getY() + 20)));
var rotation = Math.round(degrees(angle(absCenterX, absCenterY, pos.x, pos.y)));
rotation -= realRotation;
group.setRotationDeg(rotation);
return {
y: Math.round((pos.y - absCenterY) * scale + absCenterY),
x: Math.round((pos.x - absCenterX) * scale + absCenterX)
};
}
function radians(degrees) { return degrees * (Math.PI / 180); }
function degrees(radians) { return radians * (180 / Math.PI); }
// Calculate the angle between two points.
function angle(cx, cy, px, py) {
var x = cx - px;
var y = cy - py;
return Math.atan2(-y, -x);
}
// Calculate the distance between two points.
function distance(p1x, p1y, p2x, p2y) {
return Math.sqrt(Math.pow((p2x - p1x), 2) + Math.pow((p2y - p1y), 2));
}
function initStage(images) {
var stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 400
});
var darthVaderGroup = new Kinetic.Group({
x: 270,
y: 100,
draggable: true
});
var yodaGroup = new Kinetic.Group({
x: 100,
y: 110,
draggable: true
});
var layer = new Kinetic.Layer();
/*
* go ahead and add the groups
* to the layer and the layer to the
* stage so that the groups have knowledge
* of its layer and stage
*/
layer.add(darthVaderGroup);
layer.add(yodaGroup);
stage.add(layer);
// darth vader
var darthVaderImg = new Kinetic.Image({
x: 0,
y: 0,
image: images.darthVader,
width: 200,
height: 138,
name: 'image'
});
darthVaderGroup.add(darthVaderImg);
addAnchor(darthVaderGroup, -20, -20, 'topLeft', 'none');
addAnchor(darthVaderGroup, 220, -20, 'topRight', 'none');
addAnchor(darthVaderGroup, 220, 158, 'bottomRight', 'none');
addAnchor(darthVaderGroup, -20, 158, 'bottomLeft','none');
addAnchor(darthVaderGroup, 225, 0, 'rotateAnchor','rotate');
darthVaderGroup.on('dragstart', function() {
this.moveToTop();
});
stage.draw();
}
var sources = {
darthVader: 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg'
};
loadImages(sources, initStage);
</script>
</body>
</html>
You can use each anchors show/hide methods inside the images mouseenter/mouseleave events to display the anchors when the mouse enters the image:
image.on("mouseleave",function(){ anchor1.hide(); }
image.on("mouseenter",function(){ anchor1.show(); layer.draw(); }
Problem is that since your anchors are partly outside your image, so hiding the anchors when the mouse leaves the image might make the anchors "disappear" when the user intends to use them.
The ideal solution would be to listen for mouseenter/mouseleave events on the group which contains the image but also extends to include the outside part of the anchors. Unfortunately, a Kinetic.Group will not respond to mouseenter/mouseleave events.
A workaround is to create a Kinetic.Rect background to the group which includes the images plus the anchors. The rect will listen for mouseenter/mouseleave events and will show/hide the anchors. If you don't want the background rect to be visible, just set it's opacity to .001. The rect will still listen for events, but will be invisible.
groupBackgroundRect.on("mouseleave",function(){ anchor1.hide(); }
groupBackgroundRect.on("mouseenter",function(){ anchor1.show(); layer.draw(); }
A related note:
With KineticJS, combining rotation with resizing is made more difficult than it needs to be because KineticJS uses offsetX/offsetY as both an object's rotation point and as an offset to its position. Your key to making it work will be to re-center the offset point after resizing so that your rotation takes place around the new centerpoint--not the previous centerpoint. (or reset the offset reference point to any other point that you want to rotate around).

Categories

Resources