Fabric.js / canvas 无限背景网格,如 miro

问题描述 投票:0回答:1

欢呼社区,

最后,我在 StackOverflow 上遇到了第一个问题,都是关于 Fabric.js 的“无限网格” - 如果您对我的问题有任何反馈,请联系我!

项目: 我尝试使用 fabric.js 开发一个小绘图工具,并想知道如何实现像 miro 那样的无限背景网格。

我对此项目/问题的要求或个人目标

  1. 放大和缩小 - 像 miro 一样调整网格大小
  2. 平移也会像 miro 一样影响背景网格
  3. 动态画布大小 -> 如果我调整浏览器的大小,网格也会与 /height 配合画布

找到了很多有用的链接,但没有缩放、平移、调整大小或性能很差。如果您有任何“最佳实践”提示、链接或想法,请与我分享:-)

感谢您的时间和帮助!

javascript canvas grid fabricjs
1个回答
1
投票

我想我找到了一个与创建单独的类相关的解决方案,该类首先添加到画布中,以便所有内容都绘制在网格顶部。

这是代码:

const GRID_COLOR = "#000000";
const GRID_OPACITY = 0.3;
const CELL_SIZE = 20;

const MAX_SCALE = 2;
const MIN_SCALE = 0.5;

var canvas = new fabric.Canvas('myCanvas', {
    backgroundColor: 'rgb(255,165,0)',
    selectionColor: 'rgba(100,100,100,0.3)', // selection with opacity 0.3
    selectionLineWidth: 2,
    width: window.outerWidth,
    height: window.outerHeight,
    viewportTransform: [1, 0, 0, 1, -window.outerWidth/2, -window.outerHeight/2],
    //stopContextMenu: true, // prevent the right-click context menu from appearing
    fireRightClick: true, // enable firing of right click events
    fireMiddleClick: true, // enable firing of middle click events
});
window.onresize = function() {
    canvas.setWidth(window.outerWidth);
    canvas.setHeight(window.outerHeight);
};

canvas.on("mouse:wheel", function(opt) {
    let zoom = canvas.getZoom();
    zoom *= 0.999 ** opt.e.deltaY;
    if (zoom > MAX_SCALE) zoom = MAX_SCALE;
    if (zoom < MIN_SCALE) zoom = MIN_SCALE;
    
    canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
    opt.e.preventDefault();
    opt.e.stopPropagation();

});

canvas.on("mouse:up", function(opt) {
    this.setViewportTransform(this.viewportTransform);
    this.isDragging = false;
    this.selection = true;
});

canvas.on("mouse:down", function(opt) {
    if (opt.e.button === 1) {
        this.isDragging = true;
        this.selection = false;
        this.lastPosX = opt.e.clientX;
        this.lastPosY = opt.e.clientY;
    }
});

canvas.on("mouse:move", function(opt) {
    if (this.isDragging) {
        this.viewportTransform[4] += opt.e.clientX - this.lastPosX;
        this.viewportTransform[5] += opt.e.clientY - this.lastPosY;
        
        this.lastPosX = opt.e.clientX;
        this.lastPosY = opt.e.clientY;
        this.requestRenderAll();
    }
});

var infBGrid = fabric.util.createClass(fabric.Object, {
    type: 'infBGrid',
    
    initialize: function () {
        
    },
    
    render: function (ctx) {
        let zoom = canvas.getZoom();
        let offX = canvas.viewportTransform[4];
        let offY = canvas.viewportTransform[5];

        ctx.save();
        ctx.strokeStyle = "#cecece";
        ctx.lineWidth = 1;

        let gridSize = CELL_SIZE * zoom;

        const numCellsX = Math.ceil(canvas.width / gridSize);
        const numCellsY = Math.ceil(canvas.height / gridSize);

        let gridOffsetX = offX % gridSize;
        let gridOffsetY = offY % gridSize;
        ctx.beginPath();
        // draw vectical lines
        for (let i = 0; i <= numCellsX; i++) {
          let x = gridOffsetX + i * gridSize;
          ctx.moveTo((x - offX) / zoom, (0 - offY) / zoom);
          ctx.lineTo((x - offX) / zoom, (canvas.height - offY) / zoom);
          }
      
        // draw horizontal lines
        for (let i = 0; i <= numCellsY; i++) {
          let y = gridOffsetY + i * gridSize;
          ctx.moveTo((0 - offX) / zoom, (y - offY) / zoom);
          ctx.lineTo((canvas.width - offX) / zoom, (y - offY) / zoom);
        }
      
        ctx.stroke();
        ctx.closePath();
        ctx.restore();
    }
});

var bg = new infBGrid();

canvas.add(bg);
canvas.renderAll();

我想创建一个类并在渲染方法中编写代码比创建一个在移动视口时必须移动的组更好。

这是一个糟糕的解决方案,但也是一个可行的解决方案:

const GRID_COLOR = "#000000";
const GRID_OPACITY = 0.3;
const CELL_SIZE = 20;

const MAX_SCALE = 2;
const MIN_SCALE = 0.5;

let grid_size = CELL_SIZE / MIN_SCALE;

var canvas = new fabric.Canvas('myCanvas', {
    backgroundColor: 'rgb(255,165,0)',
    selectionColor: 'rgba(100,100,100,0.3)', // selection with opacity 0.3
    selectionLineWidth: 2,
    width: window.outerWidth,
    height: window.outerHeight,
    viewportTransform: [1, 0, 0, 1, -window.outerWidth/2, -window.outerHeight/2],
    //stopContextMenu: true, // prevent the right-click context menu from appearing
    fireRightClick: true, // enable firing of right click events
    fireMiddleClick: true, // enable firing of middle click events
});
window.onresize = function() {
    canvas.setWidth(window.outerWidth);
    canvas.setHeight(window.outerHeight);
};

function snapToGrid(point) {
    return Math.round(point / grid_size) * grid_size;
}

let backgroundGrid = new fabric.Group([]);
backgroundGrid.excludeFromExport = true; // when true, object is not exported in OBJECT/JSON
backgroundGrid.hasControls = false;
backgroundGrid.hasBorders = false;
backgroundGrid.evented = false;
backgroundGrid.selectable = false;

canvas.on("mouse:wheel", function(opt) {
    let zoom = canvas.getZoom();
    zoom *= 0.999 ** opt.e.deltaY;
    if (zoom > MAX_SCALE) zoom = MAX_SCALE;
    if (zoom < MIN_SCALE) zoom = MIN_SCALE;
    
    canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
    opt.e.preventDefault();
    opt.e.stopPropagation();

    backgroundGrid.setPositionByOrigin(
        new fabric.Point (
            snapToGrid(-canvas.viewportTransform[4] / canvas.getZoom()),
            snapToGrid(-canvas.viewportTransform[5] / canvas.getZoom())
        ), 
        "left", "top"
    );
});

canvas.on("mouse:down", function(opt) {
    if (opt.e.button === 1) {
        this.isDragging = true;
        this.selection = false;
        this.lastPosX = opt.e.clientX;
        this.lastPosY = opt.e.clientY;
    }
});

canvas.on("mouse:up", function(opt) {
    this.setViewportTransform(this.viewportTransform);
    this.isDragging = false;
    this.selection = true;
});

canvas.on("mouse:move", function(opt) {
    if (this.isDragging) {
        this.viewportTransform[4] += opt.e.clientX - this.lastPosX;
        this.viewportTransform[5] += opt.e.clientY - this.lastPosY;
        
        backgroundGrid.setPositionByOrigin(
            new fabric.Point (
                snapToGrid(-canvas.viewportTransform[4] / canvas.getZoom()),
                snapToGrid(-canvas.viewportTransform[5] / canvas.getZoom())
            ), 
            "left", "top"
        );

        this.requestRenderAll();
        this.lastPosX = opt.e.clientX;
        this.lastPosY = opt.e.clientY;
    }
});
function bgGrid() {
    const numCellsX = Math.ceil(canvas.width / MIN_SCALE / grid_size);
    const numCellsY = Math.ceil(canvas.height / MIN_SCALE / grid_size);

    for (let i = 0; i <= numCellsX; i++) {
        const x = i * grid_size;
        backgroundGrid.addWithUpdate(new fabric.Line([x,-grid_size,x,canvas.height / MIN_SCALE], {
            fill: 'black',
            stroke: 'black',
            strokeWidth: 1,
            opacity: GRID_OPACITY
        }));
    }

    for (let i = 0; i <= numCellsY; i++) {
        const y = i * grid_size;
        backgroundGrid.addWithUpdate(new fabric.Line([-grid_size,y,canvas.width / MIN_SCALE,y], {
            fill: 'black',
            stroke: 'black',
            strokeWidth: 1,
            opacity: GRID_OPACITY
        }));
    }
}
bgGrid();
canvas.add(backgroundGrid);

我还找到了另一个解决方案,就是重写 _renderBackground private 方法:

const GRID_COLOR = "#000000";
const GRID_OPACITY = 0.3;
const CELL_SIZE = 20;

const MAX_SCALE = 2;
const MIN_SCALE = 0.5;

function snapToGrid(point) {
    return Math.round(point / CELL_SIZE) * CELL_SIZE;
}


var canvas = new fabric.Canvas('myCanvas', {
    backgroundColor: 'rgb(0,0,0)',
    selectionColor: 'rgba(100,100,100,0.3)', // selection with opacity 0.3
    selectionLineWidth: 2,
    width: window.outerWidth,
    height: window.outerHeight,
    viewportTransform: [1, 0, 0, 1, -window.outerWidth/2, -window.outerHeight/2],
    //stopContextMenu: true, // prevent the right-click context menu from appearing
    fireRightClick: true, // enable firing of right click events
    fireMiddleClick: true, // enable firing of middle click events
});
window.onresize = function() {
    canvas.setWidth(window.outerWidth);
    canvas.setHeight(window.outerHeight);
};

canvas.on("mouse:wheel", function(opt) {
    let zoom = canvas.getZoom();
    zoom *= 0.999 ** opt.e.deltaY;
    if (zoom > MAX_SCALE) zoom = MAX_SCALE;
    if (zoom < MIN_SCALE) zoom = MIN_SCALE;
    
    canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
    opt.e.preventDefault();
    opt.e.stopPropagation();

});

canvas.on("mouse:up", function(opt) {
    this.setViewportTransform(this.viewportTransform);
    this.isDragging = false;
    this.selection = true;
});

canvas.on("mouse:down", function(opt) {
    if (opt.e.button === 1) {
        this.isDragging = true;
        this.selection = false;
        this.lastPosX = opt.e.clientX;
        this.lastPosY = opt.e.clientY;
    }
});

canvas.on("mouse:move", function(opt) {
    if (this.isDragging) {
        this.viewportTransform[4] += opt.e.clientX - this.lastPosX;
        this.viewportTransform[5] += opt.e.clientY - this.lastPosY;
        
        this.lastPosX = opt.e.clientX;
        this.lastPosY = opt.e.clientY;
        this.requestRenderAll();
    }
});


var rec = new fabric.Rect({
    left: snapToGrid(canvas.width / 2),
    top: snapToGrid(canvas.height / 2),
    width: 100,
    height: 100,
    fill: 'red',
    stroke: 'red',
    strokeWidth: 2,
    selectable: true,
    evented: true,
    draggable: true,
    angle: 0
  });


rec.on('mousedown', function(options) {
  // Drag start
  console.log('Drag start');
});

rec.on('mouseup', function(options) {
  // Drag end
  console.log('Drag end');
});

rec.on('moving', function(options) {
  // Dragging
  this.left = snapToGrid(this.left);
  this.top = snapToGrid(this.top);
});


canvas._renderBackground = function(ctx) {
    if (!this.backgroundColor) {
        return;
    }
    ctx.fillStyle = this.backgroundColor;
    ctx.fillRect(
      0,
      0,
      canvas.width,
      canvas.height
    );

    let zoom = this.getZoom();
    let offsetX = this.viewportTransform[4];
    let offsetY = this.viewportTransform[5];

    ctx.strokeStyle = "#cecece";
    ctx.lineWidth = 1;
  
    gridSize = CELL_SIZE * zoom;
  
    const numCellsX = Math.ceil(canvas.width / gridSize);
    const numCellsY = Math.ceil(canvas.height / gridSize);
              
    gridOffsetX = offsetX % gridSize;
    gridOffsetY = offsetY % gridSize;
  
    ctx.save();
    ctx.beginPath();

    for (let i = 0; i <= numCellsX; i++) {
      let x = gridOffsetX + i * gridSize;
      ctx.moveTo(x, 0);
      ctx.lineTo(x, canvas.height);
    }
  

    for (let i = 0; i <= numCellsY; i++) {
      let y = gridOffsetY + i * gridSize;
      ctx.moveTo(0, y);
      ctx.lineTo(canvas.width, y);
    }
  
    ctx.stroke();
    ctx.closePath();
    ctx.restore();
}


canvas.add(rec);

canvas.renderAll();
© www.soinside.com 2019 - 2024. All rights reserved.