高性能无限网格的绘制

1,195 阅读2分钟

绘制网格一般可以用 3 种方式绘制, css、svg、canvas

1. 通过 css 利用linear-grident 绘制

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
    }
    .grid {
      margin: 30px auto;
      width: 500px;
      height: 500px;     /* 垂直线 */                                              /* 水平线 */
      background-image: linear-gradient(to right, transparent 19px, #ccc 100%), linear-gradient(to bottom, transparent 19px, #ccc 100%);
      background-size: 20px 20px;
      background-repeat: repeat;
    }
  </style>
</head>
<body>
  <div class="grid"></div>
</body>
</html>

原理:

  1. linear-gradient(to right, transparent 19px, #ccc 100%) 绘制垂直线
  2. linear-gradient(to bottom, transparent 19px, #ccc 100%) 绘制水平线

如果要绘制大于1px的网格线可以这样写css

.grid-line {
  background-image: linear-gradient(to right, transparent 18px, #ccc 18px, #ccc 100%), linear-gradient(to bottom, transparent 18px, #ccc 18px, #ccc 100%);
      background-size: 20px 20px;
      background-repeat: repeat;
}

通过 svg 利用 pattern 填充

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <svg width="500" height="500">
    <defs>
      <pattern id="Pattern" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
        <!-- 垂直线 -->
        <line stroke="#ccc" fill="transparent" x1="19" y1="0" x2="19" y2="20"></line>
        <!-- 水平线 -->
        <line stroke="#ccc" fill="transparent" x1="0" y1="19" x2="20" y2="19"></line>
      </pattern>
    </defs>
    <rect fill="url(#Pattern)" x="0" y="0" width="500" height="500"></rect>
  </svg>
</body>
</html>

效果如下同 css grid

3. 通过 canvas 绘制 createPattern 绘制

用 canvas 绘制网格相对复杂,createPattern 参数需要传入一个 Image 对象或者另外一个canvas 元素,一般来说绘制网格直接使用css,或者svg即可 这里直接给出代码,感兴趣的可以看看

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="100%" height="100%">
    <defs>
      <pattern id="Pattern" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
        <line stroke="#ccc" fill="transparent" x1="39" y1="0" x2="39" y2="40"></line>
        <line stroke="#ccc" fill="transparent" x1="0" y1="39" x2="40" y2="39"></line>
      </pattern>
    </defs>
    <rect fill="url(#Pattern)" x="0" y="0" width="100%" height="100%"></rect>
  </svg>`
  const base64 = window.btoa(svg);
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const WIDTH = 500;
    const HEIGHT = 500;
    canvas.width = WIDTH * 2;
    canvas.height = HEIGHT * 2;
    canvas.style.width = WIDTH + 'px';
    canvas.style.height = HEIGHT + 'px';
    // ctx.scale(devicePixelRatio, devicePixelRatio);
    document.body.appendChild(canvas);
    const image = new Image();
    image.src = `data:image/svg+xml;base64,${base64}`;
    image.onload = function () {
      const pattern = ctx.createPattern(image,'repeat');
      ctx.fillStyle = pattern;
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    }
  </script>
</body>
</html>

绘制无限网格

无限网格是指,经过拖拽,缩放后,网格格子始终铺满整个渲染区域, 下面 以 html + css + js 的方式为例绘制无限网格。

  1. 让网格动起来,实现拖拽 通过改变 background-position 的方式让网格动起来
function listen(el, type, fn) {
  el.addEventListener(type, fn);
  return () => {
    el.removeEventListener(type, fn);
  }
}
const gridEl = document.getElementById('grid');
// 监听 mousedown, mousemove, mouseup 实现拖拽
// 记录鼠标按下的位置
let mousedownInfo
// background-position 初始偏移 offsetX, offsetY
const grid = {
    offsetX: 0,
    offsetY: 0,
    scale: 1,
    size: 20,
    baseSize: 20,
    setGridOffset(offsetX, offsetY) {
        this.offsetX = offsetX;
        this.offsetY = offsetY;
        gridEl.style.backgroundPosition = `${offsetX}px ${offsetY}px`;
    },
    adjustOffset() {
        // 调整背景的位置,使其数值不会过大, 背景位置 x 和 背景位置 x % grid.size 的效果一样
        grid.setGridOffset(grid.offsetX % grid.size, grid.offsetY % grid.size);
    }
},
// 初始化网格,让网格居中
initGrid();
function initGrid() {
  const rect = gridEl.getBoundingClientRect();
  const gridSize = grid.size;
  const rowCount = Math.floor(rect.height / gridSize);
  const colCount = Math.floor(rect.width / gridSize);
  const startY = -((rowCount + 1) * gridSize - rect.height) / 2;
  const startX = -((colCount + 1) * gridSize - rect.width) / 2;
  // 设置网格线
  gridEl.style.backgroundImage = `linear-gradient(to right, transparent ${size - 1}px, #ccc 100%), linear-gradient(to bottom, transparent ${size - 1}px, #ccc 100%);`
  // 让网格居中显示
  grid.setGridOffset(startX, startY);
}
// 实现拖拽
listen(gridEl, 'mousedown', (e) => {
  mousedownInfo = {
    clientX: e.clientX,
    clientY: e.clientY,
  }
  const rect = gridEl.getBoundingClientRect();
  const startX = grid.offsetX;
  const startY = grid.offsetY;
  const offMove = listen(document, 'mousemove', (e) => {
    const moveX = e.clientX - mousedownInfo.clientX;
    const moveY = e.clientY - mousedownInfo.clientY;
    // 更新background-position 让网格动起来
    grid.setGridOffset(moveX + startX, moveY + startY);
  });
  const offUp = listen(document, 'mouseup', (e) => {
    grid.adjustOffset();
    offMove();
    offUp();
  })
});
  1. 实现缩放,监听鼠标滚轮
为grid对象增加缩放函数
const grid = {
    // 指定缩放中心
    setGridScale(scale, origin) {
        const factor = scale / this.scale;
        const size = grid.baseSize * scale;
        const h = origin.x - grid.offsetX;
        const v = origin.y - grid.offsetY;
        const offsetX = origin.x - h * factor;
        const offsetY = origin.y - v * factor;
        gridEl.style.cssText += `
          background-image: linear-gradient(to right, transparent ${size - 1}px, #ccc 100%), linear-gradient(to bottom, transparent ${size - 1}px, #ccc 100%);
          background-size: ${size}px ${size}px;
          background-position: ${offsetX}px ${offsetY}px;
        `;
        grid.size = size;
        this.scale = scale;
        this.offsetX = offsetX;
        this.offsetY = offsetY;
    },
}
let timer;
document.addEventListener('mousewheel', (e) => {
  e.preventDefault();
  const delta = e.wheelDelta / 120;
  const rect = gridEl.getBoundingClientRect();
  // 以鼠标位置为缩放中心缩放
  grid.setGridScale(grid.scale + delta * 0.1, {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top,
  });
  clearTimeout(timer);
  timer = setTimeout(() => {
    grid.adjustOffset();
  }, 500)
}, {
  passive: false,
})

指定缩放中心进行缩放的计算的图示 参考: www.cnblogs.com/3body/p/943…

全部代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html,
    body {
      width: 100%;
      height: 100%;
    }
    body {
      margin: 0;
    }
    .grid {
      margin: 30px auto;
      width: 500px;
      height: 500px;
      background-repeat: repeat;
    }
  </style>
</head>
<body>
  <div id="grid" class="grid"></div>
  <script>
    const gridEl = document.getElementById('grid');
    let mousedownInfo
    const grid = {
      offsetX: 0,
      offsetY: 0,
      scale: 1,
      size: 20,
      baseSize: 20,
      setGridOffset(offsetX, offsetY) {
        this.offsetX = offsetX;
        this.offsetY = offsetY;
        gridEl.style.backgroundPosition = `${offsetX}px ${offsetY}px`;
      },
      setGridScale(scale, origin) {
        const factor = scale / this.scale;
        const size = grid.baseSize * scale;
        const h = origin.x - grid.offsetX;
        const v = origin.y - grid.offsetY;
        const offsetX = origin.x - h * factor;
        const offsetY = origin.y - v * factor;
        gridEl.style.cssText += `
          background-image: linear-gradient(to right, transparent ${size - 1}px, #ccc 100%), linear-gradient(to bottom, transparent ${size - 1}px, #ccc 100%);
          background-size: ${size}px ${size}px;
          background-position: ${offsetX}px ${offsetY}px;
        `;
        grid.size = size;
        this.scale = scale;
        this.offsetX = offsetX;
        this.offsetY = offsetY;
      },
      getGridOffset() {
        return this;
      },
      adjustOffset() {
        // 调整背景的位置,使其数值不会过大, 背景位置 x 和 背景位置 x % grid.size 的效果一样
        grid.setGridOffset(grid.offsetX % grid.size, grid.offsetY % grid.size);
      }
    }
    initGrid();
    listen(gridEl, 'mousedown', (e) => {
      mousedownInfo = {
        clientX: e.clientX,
        clientY: e.clientY,
      }
      const rect = gridEl.getBoundingClientRect();
      const startX = grid.offsetX;
      const startY = grid.offsetY;
      const offMove = listen(document, 'mousemove', (e) => {
        const moveX = e.clientX - mousedownInfo.clientX;
        const moveY = e.clientY - mousedownInfo.clientY;
        grid.setGridOffset(moveX + startX, moveY + startY);
      });
      const offUp = listen(document, 'mouseup', (e) => {
        grid.adjustOffset();
        offMove();
        offUp();
      })
    })
    let timer;
    document.addEventListener('mousewheel', (e) => {
      e.preventDefault();
      const delta = e.wheelDelta / 120;
      const rect = gridEl.getBoundingClientRect();
      // 以鼠标位置为缩放中心缩放
      grid.setGridScale(grid.scale + delta * 0.1, {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top,
      });
      clearTimeout(timer);
      timer = setTimeout(() => {
        grid.adjustOffset();
      }, 500)
    }, {
      passive: false,
    })
    function initGrid() {
      const rect = gridEl.getBoundingClientRect();
      const gridSize = 20;
      const rowCount = Math.floor(rect.height / gridSize);
      const colCount = Math.floor(rect.width / gridSize);
      const startY = -((rowCount + 1) * gridSize - rect.height) / 2;
      const startX = -((colCount + 1) * gridSize - rect.width) / 2;
      grid.setGridScale(1, {
        x: 0,
        y: 0,
      })
      grid.setGridOffset(startX, startY);
    }
    function listen(el, type, fn) {
      el.addEventListener(type, fn);
      return () => {
        el.removeEventListener(type, fn);
      }
    }
  </script>
</body>
</html>