简体   繁体   中英

How to implement canvas panning with Fabric.js

I have a Fabric.js canvas and I want to implement the full-canvas panning that software packages usually do with a "hand" tool. It's when you press one of the mouse buttons, then move over the canvas while holding the mouse button and the visible portion of the canvas changes accordingly.

You can see in this video what I want to achieve.

In order to implement this functionality I wrote the following code:

$(canvas.wrapperEl).on('mousemove', function(evt) {
    if (evt.button == 2) { // 2 is the right mouse button
        canvas.absolutePan({
            x: evt.clientX,
            y: evt.clientY
        });
    }
});

But it doesn't work. You can see in this video what happens.

How can I modify my code in order:

  1. For panning to work like in the first video?

  2. For the event handler to consume the event? It should prevent the context menu from appearing when the user presses or releases the right mouse button.

An easy way to pan a Fabric canvas in response to mouse movement is to calculate the cursor displacement between mouse events and pass it to relativePan .

Observe how we can use the screenX and screenY properties of the previous mouse event to calculate the relative position of the current mouse event:

function startPan(event) {
  if (event.button != 2) {
    return;
  }
  var x0 = event.screenX,
      y0 = event.screenY;
  function continuePan(event) {
    var x = event.screenX,
        y = event.screenY;
    fc.relativePan({ x: x - x0, y: y - y0 });
    x0 = x;
    y0 = y;
  }
  function stopPan(event) {
    $(window).off('mousemove', continuePan);
    $(window).off('mouseup', stopPan);
  };
  $(window).mousemove(continuePan);
  $(window).mouseup(stopPan);
  $(window).contextmenu(cancelMenu);
};
function cancelMenu() {
  $(window).off('contextmenu', cancelMenu);
  return false;
}
$(canvasWrapper).mousedown(startPan);

We start panning on mousedown and continue panning on mousemove . On mouseup , we cancel the panning; we also cancel the mouseup -cancelling function itself.

The right-click menu, also known as the context menu, is cancelled by returning false . The menu-cancelling function also cancels itself. Thus, the context menu will work if you subsequently click outside the canvas wrapper.

Here is a page demonstrating this approach:

http://michaellaszlo.com/so/fabric-pan/

You will see three images on a Fabric canvas (it may take a moment or two for the images to load). You'll be able to use the standard Fabric functionality. You can left-click on the images to move them around, stretch them, and rotate them. But when you right-click within the canvas container, you pan the whole Fabric canvas with the mouse.

Not sure about FabricJS, but it could be in such a way:

  1. for it to work like in the first video:

    By making use of CSS cursor property, toggling it on mousedown and mouseup events using javascript.

  2. the event handler consume the event (prevent the context menu from appearing, when the user releases the right mouse button):

    Using javascript we return false on contextmenu event

CODE: with a little problem ( * )

using jQuery JS Fiddle 1

$('#test').on('mousedown', function(e){
    if (e.button == 2) {
        // if right-click, set cursor shape to grabbing
        $(this).css({'cursor':'grabbing'});
    }
}).on('mouseup', function(){
    // set cursor shape to default
    $(this).css({'cursor':'default'});
}).on('contextmenu', function(){ 
    //disable context menu on right click
    return false;
});

Using raw javascript JS Fiddle 2

 var test = document.getElementById('test'); test.addEventListener('mousedown', function(e){ if (e.button == 2) { // if right-click, set cursor shape to grabbing this.style.cursor = 'grabbing'; } }); test.addEventListener('mouseup', function(){ // set cursor shape to default this.style.cursor = 'default'; }); test.oncontextmenu = function(){ //disable context menu on right click return false; } 


( * ) Problem:

The above snippets works as it should but there's a cross-browser issue, if you check the above fiddles in Firefox - or Opera -you'll see the correct behavior, when checked in Chrome and IE11 - didn't checked it with Edge or Safari - I found that Chrome and IE don't support the grabbing cursor shape, so in the above code snippets, change the cursor lines into this:

jQuery: $(this).css({'cursor':'move'}); JS Fiddle 3

Raw javascript: this.style.cursor = 'move'; JS Fiddle 4

Now we have a working code but without the hand cursor. but there is the following solution:-

SOLUTIONS:

  1. Chrome and Safari support grab and grabbing with the -webkit- prefix like:

     $(this).css({'cursor': '-webkit-grabbing'}); 

    but you need to make browser sniffing first, if Firefox then the default and standard code, if Chrome and Safari then with the -webkit- prefix, and this still makes IE out of the game.

  2. Have a look at this example , test it with Chrome, Safari, Firefox, Opera and IE you can see that cursor: url(foo.bar) works and supported in ALL browsers. Chrome, Safari, Firefox and Opera shows the yellow smile image smiley.gif , but IE shows the red ball cursor url(myBall.cur) .

    So I think you can make use of this, and a grabbing hand image like this 在此输入图像描述

    Or this:

    在此输入图像描述

    You can use an image like the above, a png or gif format with all browsers except IE which supports .cur , so you need to find a way to convert it into a .cur . Google search shows many result of convert image to cur

Note that , although this cursor:url(smiley.gif),url(myBall.cur),auto; - with fallback support separated by comma , works well in the W3Schools example shown above, I couldn't get it to work the same way in javascript, I tried $(this).css({'cursor': 'grabbing, move'}); but it didn't work. I also tried doing it as CSS class

.myCursor{ cursor: grabbing, -webkit-grabbing, move; }

Then with jQuery $(this).addClass('myCursor'); but no avail either.

So you still need to make browser sniffing whether you are going the second solution or a hybrid fix of the both solutions, this is my code which I've used couple times to detect browser and it worked well at the time of this post but you porbabely won't need the "Mobile" and "Kindle" parts.

 // Detecting browsers $UA = navigator.userAgent; if ($UA.match(/firefox/i)) { $browser = 'Firefox'; } else if ($UA.indexOf('Trident') != -1 && $UA.indexOf('MSIE') == -1) { $browser = 'MSIE'; } else if ($UA.indexOf('MSIE') != -1) { $browser = 'MSIE'; } else if ($UA.indexOf('OPR/') != -1) { $browser = 'Opera'; } else if ($UA.indexOf("Chrome") != -1) { $browser = 'Chrome'; } else if ($UA.indexOf("Safari")!=-1) { $browser = 'Safari'; } if($UA.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Nokia|Mobile|Opera Mini/i)) { $browser = 'Mobile'; }else if($UA.match(/KFAPWI/i)){ $browser = 'Kindle'; } console.log($browser); 


Resources:

I have an example on Github using fabric.js Canvas panning: https://sabatinomasala.github.io/fabric-clipping-demo/

The code responsible for the panning behaviour is the following: https://github.com/SabatinoMasala/fabric-clipping-demo/blob/master/src/classes/Panning.js

It's a simple extension on the fabric.Canvas.prototype , which enables you to toggle 'drag mode' on the canvas as follows:

canvas.toggleDragMode(true); // Start panning
canvas.toggleDragMode(false); // Stop panning

Take a look at the following snippet, documentation is available throughout the code.

 const STATE_IDLE = 'idle'; const STATE_PANNING = 'panning'; fabric.Canvas.prototype.toggleDragMode = function(dragMode) { // Remember the previous X and Y coordinates for delta calculations let lastClientX; let lastClientY; // Keep track of the state let state = STATE_IDLE; // We're entering dragmode if (dragMode) { // Discard any active object this.discardActiveObject(); // Set the cursor to 'move' this.defaultCursor = 'move'; // Loop over all objects and disable events / selectable. We remember its value in a temp variable stored on each object this.forEachObject(function(object) { object.prevEvented = object.evented; object.prevSelectable = object.selectable; object.evented = false; object.selectable = false; }); // Remove selection ability on the canvas this.selection = false; // When MouseUp fires, we set the state to idle this.on('mouse:up', function(e) { state = STATE_IDLE; }); // When MouseDown fires, we set the state to panning this.on('mouse:down', (e) => { state = STATE_PANNING; lastClientX = eeclientX; lastClientY = eeclientY; }); // When the mouse moves, and we're panning (mouse down), we continue this.on('mouse:move', (e) => { if (state === STATE_PANNING && e && ee) { // let delta = new fabric.Point(eemovementX, eemovementY); // No Safari support for movementX and movementY // For cross-browser compatibility, I had to manually keep track of the delta // Calculate deltas let deltaX = 0; let deltaY = 0; if (lastClientX) { deltaX = eeclientX - lastClientX; } if (lastClientY) { deltaY = eeclientY - lastClientY; } // Update the last X and Y values lastClientX = eeclientX; lastClientY = eeclientY; let delta = new fabric.Point(deltaX, deltaY); this.relativePan(delta); this.trigger('moved'); } }); } else { // When we exit dragmode, we restore the previous values on all objects this.forEachObject(function(object) { object.evented = (object.prevEvented !== undefined) ? object.prevEvented : object.evented; object.selectable = (object.prevSelectable !== undefined) ? object.prevSelectable : object.selectable; }); // Reset the cursor this.defaultCursor = 'default'; // Remove the event listeners this.off('mouse:up'); this.off('mouse:down'); this.off('mouse:move'); // Restore selection ability on the canvas this.selection = true; } }; // Create the canvas let canvas = new fabric.Canvas('fabric') canvas.backgroundColor = '#f1f1f1'; // Add a couple of rects let rect = new fabric.Rect({ width: 100, height: 100, fill: '#f00' }); canvas.add(rect) rect = new fabric.Rect({ width: 200, height: 200, top: 200, left: 200, fill: '#f00' }); canvas.add(rect) // Handle dragmode change let dragMode = false; $('#dragmode').change(_ => { dragMode = !dragMode; canvas.toggleDragMode(dragMode); }); 
 <div> <label for="dragmode"> Enable panning <input type="checkbox" id="dragmode" name="dragmode" /> </label> </div> <canvas width="300" height="300" id="fabric"></canvas> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.15/fabric.min.js"></script> 

i have made an example on jsfiddle , that we can actually drag the whole canvas with all its objects, into a parent div, like the picture,and i will try to explain it step by step.

在此输入图像描述

  1. First of all i download the drag library jquery.dradscroll.js, you can find it on the net. This is a small js file that with little changes it can helps us complete the task. download link: http://www.java2s.com/Open-Source/Javascript_Free_Code/jQuery_Scroll/Download_jquery_dragscroll_Free_Java_Code.htm

  2. create the container that holds our canvas.

      <div class="content"> <canvas id="c" width="600" height="700" ></canvas> </div> 
  3. little css

     .content{ overflow:auto; width:400px; height:400px; } 
  4. javascript:

    a. create the canvas.

    b. make default cursor , when it is over canvas , open hand

    canvas.defaultCursor = 'url(" http://maps.gstatic.com/intl/en_us/mapfiles/openhand_8_8.cur") 15 15, crosshair';

    c. override the __onMouseDown function , for change to the closedhand cursor( at end).

      fabric.Canvas.prototype.__onMouseDown = function(e){ // accept only left clicks var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; if (!isLeftClick && !fabric.isTouchSupported) { return; } if (this.isDrawingMode) { this._onMouseDownInDrawingMode(e); return; } // ignore if some object is being transformed at this moment if (this._currentTransform) { return; } var target = this.findTarget(e), pointer = this.getPointer(e, true); // save pointer for check in __onMouseUp event this._previousPointer = pointer; var shouldRender = this._shouldRender(target, pointer), shouldGroup = this._shouldGroup(e, target); if (this._shouldClearSelection(e, target)) { this._clearSelection(e, target, pointer); } else if (shouldGroup) { this._handleGrouping(e, target); target = this.getActiveGroup(); } if (target && target.selectable && !shouldGroup) { this._beforeTransform(e, target); this._setupCurrentTransform(e, target); } // we must renderAll so that active image is placed on the top canvas shouldRender && this.renderAll(); this.fire('mouse:down', { target: target, e: e }); target && target.fire('mousedown', { e: e }); if(!canvas.getActiveObject() || !canvas.getActiveGroup()){ flag=true; //change cursor to closedhand.cur canvas.defaultCursor = 'url("http://maps.gstatic.com/intl/en_us/mapfiles/closedhand_8_8.cur") 15 15, crosshair'; }//end if 
  5. override the __onMouseUp event ,to change back the cursor to openhand.

      fabric.Canvas.prototype.__onMouseUp = function(e){ if(flag){ canvas.defaultCursor = 'url(" http://maps.gstatic.com/intl/en_us/mapfiles/openhand_8_8.cur") 15 15, crosshair'; flag=false; } }; 
  6. You initialize the dragScroll() to work on the content that hosts the canvas:

      $('.content').dragScroll({}); 
  7. Some small changes on the jquery.dragScroll.js file, so as to understand when to drag the canvas and when not . On mousedown() event we add an if statement to check whether we have an active object or group.If true ,no canvas drag.

      $($scrollArea).mousedown(function (e) { if (canvas.getActiveObject() || canvas.getActiveGroup()) { console.log('no drag');return; } else { console.log($('body')); if (typeof options.limitTo == "object") { for (var i = 0; i < options.limitTo.length; i++) { if ($(e.target).hasClass(options.limitTo[i])) { doMousedown(e); } } } else { doMousedown(e); } } }); 
  8. on mousedown event we grab the DOM element (.content) and get the top & left position

      function doMousedown(e) { e.preventDefault(); down = true; x = e.pageX; y = e.pageY; top = e.target.parentElement.parentElement.scrollTop; // .content left = e.target.parentElement.parentElement.scrollLeft;// .content } 
  9. If we dont want to have the scrollbars visible:

      .content{ overflow:hidden; width:400px; height:400px; 

    }

  10. There is a small problem though,jsfiddle, accepts only https libraries ,so it blocks fabricjs, except if you add it from ' https://rawgit.com/kangax/fabric.js/master/dist/fabric.js ', but again it still blocks it some times (at least on my chrome and mozilla).

jsfiddle example : https://jsfiddle.net/tornado1979/up48rxLs/

you may have better luck ,than me, on your browser , but it would definitelly work on your live app.

Anyway, i hope helps ,good luck.

I know this has already been answered, but I redid the pen created in this answer using the new version of fabricjs (4.5.0)

Pen: https://codepen.io/201flaviosilva/pen/GROLbQa

In this case, I used the middle button in the mouse to pan:)

// Enable mouse middle button
canvas.fireMiddleClick = true;

// Mouse Up Event
if (e.button === 2) currentState = STATE_IDLE;

// Mouse Down Event
if (e.button === 2) currentState = STATE_PANNING;

:)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM