javascript - 在 HTML5 中创建可拖动和可缩放的网格

标签 javascript jquery html html5-canvas

与其他 HTML5 如何创建网格 问题不同,我想知道如何制作一个可拖动和可缩放的网格。

绘制网格非常简单:

var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
var width = window.innerWidth;
var height = window.innerHeight;

c.width = width;
c.height = height;

drawGrid(width, height, 40);

function drawGrid(gridWidth, gridHeight, boxSize) {
  ctx.clearRect(0, 0, c.width, c.height);
  ctx.beginPath();
  for (var i = 0; i <= gridWidth; i += boxSize) {
    ctx.moveTo(i, 0);
    ctx.lineTo(i, gridHeight);
  }
  for (var i = 0; i <= gridHeight; i += boxSize) {
    ctx.moveTo(0, i);
    ctx.lineTo(gridWidth, i);
  }
  ctx.strokeStyle = "rgba( 210, 210, 210, 1 )";
  ctx.stroke();
}
html,
body {
  overflow: hidden;
}

#canvas {
  position: absolute;
  top: 0;
  left: 0;
}
<canvas id="canvas"></canvas>

现在要使其可拖动,有很多方法可以做到,但我专注于创建类似于此的无限移动网格的错觉:

Example Image (抱歉,积分还不够)

随着图形向右移动,从 Canvas 尺寸中隐藏的线被移回开头,反之亦然。我不太确定如何使用鼠标移动网格以及缩放。与 SVG 不同,缩放时线条往往会变得模糊。 创建无限移动和缩放网格的最快方法是什么?

编辑: 我采用了类似的方法来移动网格,使用图像图案来填充屏幕。

var c = document.getElementById("canvas"),
  ctx = c.getContext("2d");
var width = window.innerWidth;
var height = window.innerHeight;
var itemIsSelected = false;
var clicked = function(e) {
  var x = e.pageX;
  var y = e.pageY;
}

draw(width, height);
draggable('#app');

function draw(width, height) {
  c.width = width;
  c.height = height;
  generateBackground();
}

function draggable(item) {
  var isMouseDown = false;
  document.onmousedown = function(e) {
    e.preventDefault();
    clicked.x = e.pageX;
    clicked.y = e.pageY;
    $(item).css('cursor', 'all-scroll');
    isMouseDown = true;
  };
  document.onmouseup = function(e) {
    e.preventDefault();
    isMouseDown = false;
    $(item).css('cursor', 'default');
  };
  document.onmousemove = function(e) {
    e.preventDefault();
    if (isMouseDown == true) {
      var mouseX = e.pageX;
      var mouseY = e.pageY;
      generateBackground(mouseX, mouseY, clicked.x, clicked.y);
    }
  };
}

function generateBackground(x, y, initX, initY) {
  distanceX = x - initX;
  distanceY = y - initY;
  ctx.clearRect(0, 0, c.width, c.height);
  var bgImage = document.getElementById("bg")
  var pattern = ctx.createPattern(bgImage, "repeat");
  ctx.rect(0, 0, width, height);
  ctx.fillStyle = pattern;
  ctx.fill();
  ctx.translate(Math.sqrt(distanceX), Math.sqrt(distanceY));
}
html,
body {
  overflow: hidden;
}

#canvas {
  top: 0;
  left: 0;
  position: absolute;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas"></canvas>
<img src="https://i.imgur.com/2MupHjw.png" id="bg" hidden>

此方法不允许我向左或向上滚动。它还会加快一段距离后的拖动速度,并且不能很好地处理负运动。

最佳答案

平移和缩放

我没有太多的时间,所以代码将不得不做大部分的解释。

下面的示例使用鼠标和鼠标滚轮进行平移和缩放。

对象 panZoom 包含有关缩放 (scale) 和平移位置 (x, y) 的信息)

要平移,只需使用鼠标位置的变化来改变 panZoom x, y 位置。您不需要缩放鼠标移动,因为它们不受缩放的影响。

缩放是通过函数 panZoom.scaleAt(x,y,scale) 其中 x, y 是鼠标位置和 scaleBy是缩放比例的量。例如 panZoom.scaleAt(100,100, 2) 将在位置 100,100 处放大 2 倍,而 panZoom.scaleAt(100,100, 1/2) 将同时缩小 2 倍位置。有关详细信息,请参阅 update 函数。

要在 panZoom 坐标系中绘制,您需要调用函数 panZoom.apply 来设置上下文转换以匹配 panZoom 的设置。函数 drawGrid 就是一个例子。它绘制一个网格以适应当前的平移和缩放。

请注意,要恢复正常的屏幕坐标,只需调用 ctx.setTransform(1,0,0,1,0,0),如果您想清除 Canvas ,则需要这样做。

片段更新 2021

我已经添加了

  • drawGrid(gridScreenSize, adaptive) 的参数

    • gridScreenSize 屏幕像素中的网格大小(自适应模式)。在世界像素中(静态模式)

    • adaptive 如果 true 网格比例适应世界比例。 false 网格大小是固定的。当 false 常量 gridLimit 设置要渲染的最大行数

  • scaleRate 控制缩放率的常量。查看评论了解更多。

  • 显示文本以显示当前比例。

  • 用于在静态和自适应网格大小之间切换的复选框。

const ctx = canvas.getContext("2d");
requestAnimationFrame(update);
const mouse  = {x : 0, y : 0, button : false, wheel : 0, lastX : 0, lastY : 0, drag : false};
const gridLimit = 64;  // max grid lines for static grid
const gridSize = 128;  // grid size in screen pixels for adaptive and world pixels for static
const scaleRate = 1.02; // Closer to 1 slower rate of change
                        // Less than 1 inverts scaling change and same rule
                        // Closer to 1 slower rate of change
const topLeft = {x: 0, y: 0};  // holds top left of canvas in world coords.


function mouseEvents(e){
    const bounds = canvas.getBoundingClientRect();
    mouse.x = e.pageX - bounds.left - scrollX;
    mouse.y = e.pageY - bounds.top - scrollY;
    mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
    if(e.type === "wheel"){
        mouse.wheel += -e.deltaY;
        e.preventDefault();
    }
}
["mousedown", "mouseup", "mousemove"].forEach(name => document.addEventListener(name,mouseEvents));
document.addEventListener("wheel",mouseEvents, {passive: false});

const panZoom = {
    x : 0,
    y : 0,
    scale : 1,
    apply() { ctx.setTransform(this.scale, 0, 0, this.scale, this.x, this.y) },
    scaleAt(x, y, sc) {  // x & y are screen coords, not world
        this.scale *= sc;
        this.x = x - (x - this.x) * sc;
        this.y = y - (y - this.y) * sc;
    },
    toWorld(x, y, point = {}) {   // converts from screen coords to world coords
        const inv = 1 / this.scale;
        point.x = (x - this.x) * inv;
        point.y = (y - this.y) * inv;
        return point;
    },
}
function drawGrid(gridScreenSize = 128, adaptive = true){
    var scale, gridScale, size, x, y, limitedGrid = false;
    if (adaptive) {
        scale = 1 / panZoom.scale;
        gridScale = 2 ** (Math.log2(gridScreenSize * scale) | 0);
        size = Math.max(w, h) * scale + gridScale * 2;
        x = ((-panZoom.x * scale - gridScale) / gridScale | 0) * gridScale;
        y = ((-panZoom.y * scale - gridScale) / gridScale | 0) * gridScale;
    } else {
        gridScale = gridScreenSize;
        size = Math.max(w, h) / panZoom.scale + gridScale * 2;
        panZoom.toWorld(0,0, topLeft);
        x = Math.floor(topLeft.x / gridScale) * gridScale;
        y = Math.floor(topLeft.y / gridScale) * gridScale;
        if (size / gridScale > gridLimit) {
            size = gridScale * gridLimit;
            limitedGrid = true;
        }            
    } 
    panZoom.apply();
    ctx.lineWidth = 1;
    ctx.strokeStyle = "#000";
    ctx.beginPath();
    for (i = 0; i < size; i += gridScale) {
        ctx.moveTo(x + i, y);
        ctx.lineTo(x + i, y + size);
        ctx.moveTo(x, y + i);
        ctx.lineTo(x + size, y + i);
    }
    ctx.setTransform(1, 0, 0, 1, 0, 0); // reset the transform so the lineWidth is 1
    ctx.stroke();
    
    info.textContent = "Scale: 1px = " + (1/panZoom.scale).toFixed(4) + " world px ";
    limitedGrid && (info.textContent += " Static grid limit " + (gridLimit * gridLimit) + " cells");    
}   
function drawPoint(x, y) {
    const worldCoord = panZoom.toWorld(x, y);
    panZoom.apply();
    ctx.lineWidth = 1;
    ctx.strokeStyle = "red";
    ctx.beginPath();   
    ctx.moveTo(worldCoord.x - 10, worldCoord.y);
    ctx.lineTo(worldCoord.x + 10, worldCoord.y);
    ctx.moveTo(worldCoord.x, worldCoord.y - 10);
    ctx.lineTo(worldCoord.x, worldCoord.y + 10); 
    ctx.setTransform(1, 0, 0, 1, 0, 0); //reset the transform so the lineWidth is 1
    ctx.stroke();  
}

var w = canvas.width;
var h = canvas.height;
function update(){
    ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    if (w !== innerWidth || h !== innerHeight) {
        w = canvas.width = innerWidth;
        h = canvas.height = innerHeight;
    } else {
        ctx.clearRect(0, 0, w, h);
    }
    if (mouse.wheel !== 0) {
        let scale = 1;
        scale = mouse.wheel < 0 ? 1 / scaleRate : scaleRate;
        mouse.wheel *= 0.8;
        if(Math.abs(mouse.wheel) < 1){
            mouse.wheel = 0;
        }
        panZoom.scaleAt(mouse.x, mouse.y, scale); //scale is the change in scale
    }
    if (mouse.button) {
       if (!mouse.drag) {
          mouse.lastX = mouse.x;
          mouse.lastY = mouse.y;
          mouse.drag = true;
       } else {
          panZoom.x += mouse.x - mouse.lastX;
          panZoom.y += mouse.y - mouse.lastY;
          mouse.lastX = mouse.x;
          mouse.lastY = mouse.y;
       }
    } else if (mouse.drag) {
        mouse.drag = false;
    }
    drawGrid(gridSize, adaptiveGridCb.checked);
    drawPoint(mouse.x, mouse.y);
    requestAnimationFrame(update);
}
canvas { position : absolute; top : 0px; left : 0px; }
div { 
  position : absolute; 
  top : 5px; 
  left : 5px; 
  font-family: arial;
  font-size: 16px;
  background: #FFFD;
}
<canvas id="canvas"></canvas>
<div>
<label for="adaptiveGridCb">Adaptive grid</label>
<input id="adaptiveGridCb" type="checkbox" checked/>
<span id="info"></span>
</div>

关于javascript - 在 HTML5 中创建可拖动和可缩放的网格,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53310138/

相关文章:

javascript - 如何删除和附加元素 li?

javascript - 单击按钮后如何逐一显示表单字段

javascript - JQuery/Javascript 将 HTML 表数据附加到 TBODY,双输出

javascript - 删除 Bootstrap 4 Collapse 元素的过渡和延迟

jquery - 事件管理 - 根据第一个下拉菜单的选择刷新第二个下拉菜单 (Rails 3.2)

jquery - 在悬停/鼠标悬停时,文本不会随页内滚动条移动

javascript - 将重复项放在公共(public)元素下

jquery - 如何向 slider 添加填充?

javascript - 如何检查选取框是否滚动

javascript - 文本区域中的句柄选项卡