Skip to main content

HTML5 大画布滚动演示

想象一下,我们有这样一个场景。一个非常大的舞台 3000x3000,里面有许多节点。 用户希望查看所有节点,但它们一次无法全部显示。

如何显示和滚动一个非常大的 HTML5 画布?

想一想,你有一个非常大的画布,并且想要添加在上面导航的能力。

我将向你展示 4 种不同的方法来实现这一点:

1. 仅仅制作大舞台

这是最简单的方法。但它非常慢,因为大画布的渲染速度较慢。 用户将能够使用本地滚动条进行滚动。

优点:

  • 实现简单

缺点:

  • 速度慢
import Konva from 'konva';

const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;

const stage = new Konva.Stage({
  container: 'container',
  width: WIDTH,
  height: HEIGHT,
});

const layer = new Konva.Layer();
stage.add(layer);

function generateNode() {
  return new Konva.Circle({
    x: WIDTH * Math.random(),
    y: HEIGHT * Math.random(),
    radius: 50,
    fill: 'red',
    stroke: 'black',
  });
}

for (let i = 0; i < NUMBER; i++) {
  layer.add(generateNode());
}

2. 使舞台可拖动(通过拖放导航)

这个方法更好,因为舞台的尺寸要小得多。

优点:

  • 实现简单
  • 快速

缺点:

  • 有时拖放导航不是最佳用户体验
import Konva from 'konva';

const width = window.innerWidth;
const height = window.innerHeight;

const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
  draggable: true,
});

const layer = new Konva.Layer();
stage.add(layer);

const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;

function generateNode() {
  return new Konva.Circle({
    x: WIDTH * Math.random(),
    y: HEIGHT * Math.random(),
    radius: 50,
    fill: 'red',
    stroke: 'black',
  });
}

for (let i = 0; i < NUMBER; i++) {
  layer.add(generateNode());
}

3. 模拟滚动条

你需要手动绘制滚动条并实现所有移动功能。这将涉及大量工作,但在许多应用中效果良好。

指示:尝试通过滚动条进行滚动。

优点:

  • 运行良好
  • 直观的滚动体验
  • 快速

缺点:

  • 滚动条不是本地的,因此你需要手动实现许多功能(如键盘滚动)
import Konva from 'konva';

const width = window.innerWidth;
const height = window.innerHeight;

const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

const layer = new Konva.Layer();
stage.add(layer);

const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;

function generateNode() {
  return new Konva.Circle({
    x: WIDTH * Math.random(),
    y: HEIGHT * Math.random(),
    radius: 50,
    fill: 'red',
    stroke: 'black',
  });
}

for (let i = 0; i < NUMBER; i++) {
  layer.add(generateNode());
}

// 现在绘制我们的滚动条
const scrollLayers = new Konva.Layer();
stage.add(scrollLayers);

const PADDING = 5;

const verticalBar = new Konva.Rect({
  width: 10,
  height: 100,
  fill: 'grey',
  opacity: 0.8,
  x: stage.width() - PADDING - 10,
  y: PADDING,
  draggable: true,
  dragBoundFunc: function (pos) {
    pos.x = stage.width() - PADDING - 10;
    pos.y = Math.max(
      Math.min(pos.y, stage.height() - this.height() - PADDING),
      PADDING
    );
    return pos;
  },
});
scrollLayers.add(verticalBar);

verticalBar.on('dragmove', function () {
  // 百分比的增量
  const availableHeight =
    stage.height() - PADDING * 2 - verticalBar.height();
  const delta = (verticalBar.y() - PADDING) / availableHeight;

  layer.y(-(HEIGHT - stage.height()) * delta);
});

const horizontalBar = new Konva.Rect({
  width: 100,
  height: 10,
  fill: 'grey',
  opacity: 0.8,
  x: PADDING,
  y: stage.height() - PADDING - 10,
  draggable: true,
  dragBoundFunc: function (pos) {
    pos.x = Math.max(
      Math.min(pos.x, stage.width() - this.width() - PADDING),
      PADDING
    );
    pos.y = stage.height() - PADDING - 10;

    return pos;
  },
});
scrollLayers.add(horizontalBar);

horizontalBar.on('dragmove', function () {
  // 百分比的增量
  const availableWidth =
    stage.width() - PADDING * 2 - horizontalBar.width();
  const delta = (horizontalBar.x() - PADDING) / availableWidth;

  layer.x(-(WIDTH - stage.width()) * delta);
});

stage.on('wheel', function (e) {
  // 防止父级滚动
  e.evt.preventDefault();
  const dx = e.evt.deltaX;
  const dy = e.evt.deltaY;

  const minX = -(WIDTH - stage.width());
  const maxX = 0;

  const x = Math.max(minX, Math.min(layer.x() - dx, maxX));

  const minY = -(HEIGHT - stage.height());
  const maxY = 0;

  const y = Math.max(minY, Math.min(layer.y() - dy, maxY));
  layer.position({ x, y });

  const availableHeight =
    stage.height() - PADDING * 2 - verticalBar.height();
  const vy =
    (layer.y() / (-HEIGHT + stage.height())) * availableHeight + PADDING;
  verticalBar.y(vy);

  const availableWidth =
    stage.width() - PADDING * 2 - horizontalBar.width();

  const hx =
    (layer.x() / (-WIDTH + stage.width())) * availableWidth + PADDING;
  horizontalBar.x(hx);
});

4. 模拟屏幕移动,使用变换

该演示非常顺畅,但可能会有些复杂。 其思路是:

  • 我们将使用一个小画布,大小与屏幕相同
  • 我们将创建一个所需大小的容器(3000x3000),因此可以显示本地滚动条
  • 当用户尝试滚动时,我们会为舞台容器应用 CSS 变换,以使其仍然位于用户屏幕的中心
  • 我们将移动所有节点,以使其看起来像是在滚动(通过改变舞台位置)

优点:

  • 运行完美且快速
  • 本地滚动

缺点:

  • 你必须理解发生了什么。

指示:尝试通过本地滚动条进行滚动。

import Konva from 'konva';

// 首先我们需要添加所需的 CSS
const style = document.createElement('style');
style.textContent = `
  #large-container {
    width: 3000px;
    height: 3000px;
    overflow: hidden;
  }

  #scroll-container {
    width: calc(100% - 22px);
    height: calc(100vh - 22px);
    overflow: auto;
    margin: 10px;
    border: 1px solid grey;
  }
`;
document.head.appendChild(style);

// 然后创建所需的 DOM 结构
const scrollContainer = document.createElement('div');
scrollContainer.id = 'scroll-container';
const largeContainer = document.createElement('div');
largeContainer.id = 'large-container';
const container = document.createElement('div');
container.id = 'stage-container';

scrollContainer.appendChild(largeContainer);
largeContainer.appendChild(container);
document.body.appendChild(scrollContainer);

const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;

// padding 增加了舞台的大小
// 因此滚动看起来会更顺畅
const PADDING = 200;

const stage = new Konva.Stage({
  container: 'stage-container',
  width: window.innerWidth + PADDING * 2,
  height: window.innerHeight + PADDING * 2,
});

const layer = new Konva.Layer();
stage.add(layer);

function generateNode() {
  return new Konva.Circle({
    x: WIDTH * Math.random(),
    y: HEIGHT * Math.random(),
    radius: 50,
    fill: 'red',
    stroke: 'black',
  });
}

for (let i = 0; i < NUMBER; i++) {
  layer.add(generateNode());
}

function repositionStage() {
  const dx = scrollContainer.scrollLeft - PADDING;
  const dy = scrollContainer.scrollTop - PADDING;
  stage.container().style.transform =
    'translate(' + dx + 'px, ' + dy + 'px)';
  stage.x(-dx);
  stage.y(-dy);
}
scrollContainer.addEventListener('scroll', repositionStage);
repositionStage();