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

2022-01-17 00:00:00 jquery javascript html html5-canvas

与其他 HTML5如何创建网格 问题不同,我想知道如何使一个可拖动和可扩展的问题.

绘制网格非常简单:

var c = document.getElementById('canvas');var ctx = c.getContext('2d');var 宽度 = window.innerWidth;变量高度 = window.innerHeight;c.宽度=宽度;c.height = 高度;绘制网格(宽度,高度,40);函数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,身体 {溢出:隐藏;}#帆布 {位置:绝对;顶部:0;左:0;}

<canvas id="canvas"></canvas>

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

示例图片(抱歉,积分还不够)

当图表向右移动时,从画布大小隐藏的线条将移回起点,反之亦然.我不太清楚如何用鼠标移动网格以及缩放.与 SVG 不同,这些线条在缩放时往往会变得模糊.创建无限移动和缩放网格的最快方法是什么?

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

var c = document.getElementById("canvas"),ctx = c.getContext("2d");var 宽度 = window.innerWidth;变量高度 = window.innerHeight;var itemIsSelected = false;var clicked = function(e) {var x = e.pageX;var y = e.pageY;}绘制(宽度,高度);可拖动('#app');函数绘制(宽度,高度){c.宽度=宽度;c.height = 高度;生成背景();}功能可拖动(项目){var isMouseDown = false;document.onmousedown = 函数(e){e.preventDefault();clicked.x = e.pageX;clicked.y = e.pageY;$(item).css('cursor', 'all-scroll');isMouseDown = true;};document.onmouseup = 函数(e){e.preventDefault();isMouseDown = 假;$(item).css('cursor', 'default');};document.onmousemove = 函数(e){e.preventDefault();如果(isMouseDown == true){var mouseX = e.pageX;var mouseY = e.pageY;generateBackground(mouseX, mouseY, clicked.x, clicked.y);}};}函数生成背景(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, 宽度, 高度);ctx.fillStyle = 图案;ctx.fill();ctx.translate(Math.sqrt(distanceX), Math.sqrt(distanceY));}

html,身体 {溢出:隐藏;}#帆布 {顶部:0;左:0;位置:绝对;}

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></脚本><canvas id="canvas"></canvas><img src="https://i.imgur.com/2MupHjw.png" id="bg" 隐藏>

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

解决方案

平移和缩放

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

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

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) 如果要清除画布,则需要执行此操作.

2021 年代码段更新

我已经添加了

  • drawGrid(gridScreenSize,adaptive)的参数

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

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

  • scaleRate 控制缩放速率的常量.查看更多评论.

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

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

const ctx = canvas.getContext("2d");requestAnimationFrame(更新);const mouse = {x : 0, y : 0, button : false, wheel : 0, lastX : 0, lastY : 0, drag : false};常量 gridLimit = 64;//静态网格的最大网格线常量网格大小 = 128;//自适应的屏幕像素网格大小和静态的世界像素常数 scaleRate = 1.02;//接近 1 更慢的变化率//小于 1 反转缩放变化和相同的规则//接近 1 更慢的变化率常量 topLeft = {x: 0, y: 0};//以世界坐标保持画布的左上角.功能鼠标事件(e){const bounds = canvas.getBoundingClientRect();mouse.x = e.pageX - bounds.left - scrollX;mouse.y = e.pageY - bounds.top - scrollY;mouse.button = e.type === "mousedown" ?真:e.type === "mouseup" ?假:鼠标按钮;如果(e.type ===轮子"){mouse.wheel += -e.deltaY;e.preventDefault();}}["mousedown", "mouseup", "mousemove"].forEach(name => document.addEventListener(name,mouseEvents));document.addEventListener("wheel",mouseEvents, {passive: false});常量 panZoom = {x : 0,是:0,规模:1,apply() { ctx.setTransform(this.scale, 0, 0, this.scale, this.x, this.y) },scaleAt(x, y, sc) {//x &y 是屏幕坐标,而不是世界坐标this.scale *= sc;this.x = x - (x - this.x) * sc;this.y = y - (y - this.y) * sc;},toWorld(x, y, point = {}) {//从屏幕坐标转换为世界坐标常量 inv = 1/this.scale;point.x = (x - this.x) * inv;point.y = (y - this.y) * inv;返回点;},}函数drawGrid(gridScreenSize = 128,自适应=真){var scale,gridScale,大小,x,y,limitedGrid = false;如果(自适应){比例 = 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;} 别的 {gridScale = gridScreenSize;大小 = 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) {大小 = gridScale * gridLimit;有限网格=真;}}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 + 大小);ctx.moveTo(x, y + i);ctx.lineTo(x + size, y + i);}ctx.setTransform(1, 0, 0, 1, 0, 0);//重置变换,使 lineWidth 为 1ctx.stroke();info.textContent = "比例:1px = " + (1/panZoom.scale).toFixed(4) + " world px ";有限网格&&(info.textContent += " 静态网格限制 " + (gridLimit * gridLimit) + " 单元格");}函数drawPoint(x,y){常量 worldCoord = panZoom.toWorld(x, y);panZoom.apply();ctx.lineWidth = 1;ctx.strokeStyle = "红色";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);//重置变换,使lineWidth为1ctx.stroke();}var w = canvas.width;var h = canvas.height;函数更新(){ctx.setTransform(1, 0, 0, 1, 0, 0);//重置变换ctx.globalAlpha = 1;//重置阿尔法if (w !== innerWidth || h !== innerHeight) {w = canvas.width = 内部宽度;h = canvas.height = innerHeight;} 别的 {ctx.clearRect(0, 0, w, h);}如果(鼠标滚轮!== 0){让比例 = 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是比例的变化}如果(鼠标按钮){如果(!mouse.drag){鼠标.lastX = 鼠标.x;鼠标.lastY = 鼠标.y;mouse.drag = true;} 别的 {panZoom.x += mouse.x - mouse.lastX;panZoom.y += mouse.y - mouse.lastY;鼠标.lastX = 鼠标.x;鼠标.lastY = 鼠标.y;}} else if (mouse.drag) {鼠标拖动 = 假;}drawGrid(gridSize,adaptiveGridCb.checked);绘图点(鼠标.x,鼠标.y);requestAnimationFrame(更新);}

canvas { position : absolute;顶部:0px;左:0px;}分区 {位置:绝对;顶部:5px;左:5px;字体系列:arial;字体大小:16px;背景:#FFFD;}

<canvas id="canvas"></canvas>

<label for="adaptiveGridCb">自适应网格</label><input id="adaptiveGridCb" type="checkbox" 勾选/><span id="info"></span></div>

Unlike the other HTML5 how to create a grid questions, I'm wondering how to make one draggable and scalable.

Drawing the grid is pretty simple:

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>

Now to make it draggable, there are many ways of doing it, but I focused on creating the illusion of an infinite moving grid similar to this:

Example Image (Sorry, don't have enough credits yet)

As the graph moves toward the right, the lines that are hidden from the canvas size are moved back to the beginning, and vice versa. I'm not too sure how to go about moving the grid with the mouse, as well as the scaling. The lines tend to blur when scaled unlike SVGs. What's the quickest way of creating a grid to move around infinitely and scale?

EDIT: I took a similar approach to move the grid around using an image pattern to fill the screen.

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>

This method does not allow me to scroll left or up. It also speeds up the dragging after a distance and does not deal with negative motion well.

解决方案

Pan and Zoom

I dont have much tine so the code will have to do most of the explaining.

The example below pans and zooms using the mouse and mouse wheel.

The object panZoom holds the information regarding the zoom (scale) and pan position (x, y)

To pan just use the change in mouse position to change the panZoom x, y position. You dont need to scale the mouse movements as they are not effected by the zoom.

The zoom is via a function panZoom.scaleAt(x,y,scale) where x, y is the mouse position and scaleBy is the amount to scale the scale by. Eg panZoom.scaleAt(100,100, 2) will zoom in 2 times at position 100,100 and panZoom.scaleAt(100,100, 1/2) will zoom out 2 times at the same position. See update function for more details.

To draw in the panZoom coordinate system you need to call the function panZoom.apply which sets the context transform to match panZoom's settings. The function drawGrid is an example. It draws a grid to fit the current pan and zoom.

Note that to restore the normal screen coordinates just call ctx.setTransform(1,0,0,1,0,0) which you will need to do if you want to clear the canvas.

Snippet update 2021

I have added

  • arguments for drawGrid(gridScreenSize, adaptive)

    • gridScreenSize size of grid in screen pixels (adaptive mode). In world pixels (static mode)

    • adaptive if true grid scale adapts to world scale. false grid size is fixed. When false the constant gridLimit sets the max number of lines to render

  • scaleRate constant that controls the rate of scaling. See comments for more.

  • display text to show current scale.

  • checkbox to switch between static and adaptive grid size.

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>

相关文章