首页 > 解决方案 > 在 HTML5 中创建可拖动和可缩放的网格

问题描述

与其他 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>

现在要让它可拖动,有很多方法可以做到,但我专注于创造类似于这样的无限移动网格的错觉:

示例图片 (抱歉,还没有足够的积分)

当图形向右移动时,从画布大小隐藏的线条会移回起点,反之亦然。我不太清楚如何用鼠标移动网格以及缩放。与 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>

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

标签: javascriptjqueryhtmlhtml5-canvas

解决方案


平移和缩放

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

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

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

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

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

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

请注意,要恢复正常的屏幕坐标,只需调用ctx.setTransform(1,0,0,1,0,0)如果您想清除画布,您需要执行此操作。

片段更新 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>


推荐阅读