Как реализовать панорамирование холста с помощью Fabric.js

У меня есть холст Fabric.js, и я хочу реализовать полномасштабное панорамирование, которое пакеты программного обеспечения обычно выполняют с помощью "ручного" инструмента. Это когда вы нажимаете одну из кнопок мыши, затем перемещайтесь по холсту, удерживая кнопку мыши, и видимая часть холста изменяется соответственно.

Вы можете видеть в этом видео, чего я хочу достичь.

Чтобы реализовать эту функциональность, я написал следующий код:

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

Но это не сработает. Вы можете видеть в этом видео, что происходит.

Как изменить код в следующем порядке:

  • Для панорамирования для работы, как в первом видео?

  • Чтобы обработчик события использовал событие? Он должен помешать появлению контекстного меню, когда пользователь нажимает или отпускает правую кнопку мыши.

Ответы

Ответ 1

Легкий способ панорамирования холста Fabric в ответ на движение мыши - это вычислить смещение курсора между событиями мыши и передать его на relativePan.

Обратите внимание, как мы можем использовать свойства screenX и screenY предыдущего события мыши для вычисления относительного положения текущего события мыши:

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);

Мы начинаем панорамирование на mousedown и продолжаем панорамирование на mousemove. На mouseup мы отменим панорамирование; мы также отменяем функцию mouseup -cancelling.

Меню правой кнопки мыши, также известное как контекстное меню, отменяется возвратом false. Функция отмены меню также отменяет себя. Таким образом, контекстное меню будет работать, если вы впоследствии щелкните за пределами оболочки холста.

Вот страница, демонстрирующая этот подход:

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

Вы увидите три изображения на холсте ткани (для загрузки изображений может потребоваться время или два). Вы сможете использовать стандартную функциональность Fabric. Вы можете щелкнуть левой кнопкой мыши по изображениям, чтобы перемещать их, растягивать и вращать. Но когда вы щелкните правой кнопкой мыши в контейнере canvas, вы перемещаете весь холст Ткань с помощью мыши.

Ответ 2

Не уверен в FabricJS, но он может быть таким:

  • чтобы он работал как в первом видео:

    Используя свойство CSS cursor, переключая его на события mousedown и mouseup, используя javascript.

  • обработчик события потребляет событие (предотвращение появления контекстного меню, когда пользователь отпускает правую кнопку мыши):

    Используя javascript, мы возвращаем false в contextmenu событие

КОД: с небольшой проблемой (*)

с помощью 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;
});

Использование 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;
}

Ответ 3

Я сделал пример на jsfiddle, что мы можем фактически перетащить весь холст со всеми его объектами в родительский div, как и изображение, и я попытаюсь объяснить его шаг за шагом.

введите описание изображения здесь

  • Прежде всего, я загружаю библиотеку перетаскивания jquery.dradscroll.js, вы можете найти ее в сети. Это небольшой файл js, который с небольшими изменениями может помочь нам выполнить задачу. ссылка для скачивания: http://www.java2s.com/Open-Source/Javascript_Free_Code/jQuery_Scroll/Download_jquery_dragscroll_Free_Java_Code.htm

  • создайте контейнер, содержащий наш холст.

      <div class="content">
        <canvas id="c" width="600" height="700" ></canvas>    
      </div>
    
  • немного css

    .content{
       overflow:auto;
       width:400px;
       height:400px;
    }
    
  • JavaScript:

    а. создайте холст.

    б. сделать по умолчанию курсор, когда он находится над холстом, открыть руку

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

    с. переопределить функцию __onMouseDown, для перехода на закрытый курсор (в конце).

         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
    
  • переопределить событие __onMouseUp, чтобы изменить курсор на открытую.

       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;
           }
        };
    
  • Вы инициализируете dragScroll() для работы с содержимым, на котором размещается холст:

        $('.content').dragScroll({});
    
  • Небольшие изменения в файле jquery.dragScroll.js, чтобы понять, когда нужно перетаскивать холст, а не. В событии mousedown() мы добавляем оператор if, чтобы проверить, есть ли у нас активный объект или группа. Если это правда, перетяните холст.

       $($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);
            }
        }
    });
    
  • в событии mousedown мы захватываем элемент DOM (.content) и получаем верхнюю и левую позицию

      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
        }
    
  • Если мы не хотим видеть полосы прокрутки:

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

    }

  • Есть небольшая проблема, хотя jsfiddle принимает только библиотеки https, поэтому она блокирует fabricjs, за исключением того, что вы добавляете ее из https://rawgit.com/kangax/fabric.js/master/dist/fabric.js ', но снова он по-прежнему блокирует его несколько раз (по крайней мере, на моем хроме и мозилле).

Пример jsfiddle: https://jsfiddle.net/tornado1979/up48rxLs/

вам может быть повезло больше, чем мне, в вашем браузере, но это определенно будет работать над вашим живым приложением.

В любом случае, надеюсь, поможет, удачи.

Ответ 4

У меня есть пример в Github с использованием fabric.js Canvas panning: https://sabatinomasala.github.io/fabric-clipping-demo/

Код, ответственный за поведение панорамирования, следующий: https://github.com/SabatinoMasala/fabric-clipping-demo/blob/master/src/classes/Panning.js

Это простое расширение на fabric.Canvas.prototype, которое позволяет вам переключать режим перетаскивания на холст следующим образом:

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

Взгляните на следующий фрагмент, документация доступна по всему коду.

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 = e.e.clientX;
      lastClientY = e.e.clientY;
    });
    // When the mouse moves, and we're panning (mouse down), we continue
    this.on('mouse:move', (e) => {
      if (state === STATE_PANNING && e && e.e) {
        // let delta = new fabric.Point(e.e.movementX, e.e.movementY); // 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 = e.e.clientX - lastClientX;
        }
        if (lastClientY) {
          deltaY = e.e.clientY - lastClientY;
        }
        // Update the last X and Y values
        lastClientX = e.e.clientX;
        lastClientY = e.e.clientY;

        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>