固定字体大小的HTML画布,如何在缩放时使其改变

3
我之前用HTML画布制作了笛卡尔坐标系。一位用户通过鼠标帮助我添加了缩放功能。
但是我有一个问题。轴上数字的字体大小是固定的,因此在缩小时,字体大小也变小了。
我想要一个固定的字体大小,但数字之间的间隔可以改变。
例如,如果您放大,您将看到x轴上的数字0、1、2、3、4、5,
但一旦您缩小它应该是0、5、10、15,
就像Geogebra https://www.geogebra.org/classic 一样。
我需要制作自己的坐标系,不能使用小程序或嵌入式代码来进行项目。
我现在拥有的代码。

class ViewPort {
    constructor(canvas) {
      this.canvas = canvas

      /**
        * Point used to calculate the change of every point's position on
        * canvas after view port is zoomed and panned
        */
      this.center = this.basicCenter

      this.zoom = 1

      this.shouldPan = false
      this.prevZoomingPoint = null
    }

    get canvasWidth() {
      return this.canvas.getBoundingClientRect().width
    }

    get canvasHeight() {
      return this.canvas.getBoundingClientRect().height
    }

    get canvasLeft() {
      return this.canvas.getBoundingClientRect().left
    }

    get canvasTop() {
      return this.canvas.getBoundingClientRect().top
    }

    get context() {
      return this.canvas.getContext('2d')
    }

    get basicCenter() {
      const { canvasWidth, canvasHeight } = this

      const point = {
        x: canvasWidth / 2,
        y: canvasHeight / 2
      }
      return point
    }

    get basicWidth() {
      const width = this.canvasWidth
      return width
    }

    get basicHeight() {
      const height = this.canvasHeight
      return height
    }

    get width() {
      const { basicWidth, zoom } = this
      const width = basicWidth * zoom
      return width
    }

    get height() {
      const { basicHeight, zoom } = this
      const height = basicHeight * zoom
      return height
    }

    get movement() {
      const { width, height, basicWidth, basicHeight } = this
      const { x: cx, y: cy } = this.center
      const { x: basicCX, y: basicCY } = this.basicCenter

      const deltaX = cx - basicCX - ((width - basicWidth) / 2)
      const deltaY = cy - basicCY - ((height - basicHeight) / 2)
      const res = {
        x: deltaX,
        y: deltaY
      }

      return res
    }

    get pan() {
      const { center, zoom, basicCenter } = this
      const res = {
        x: center.x - basicCenter.x,
        y: center.y - basicCenter.y
      }
      return res
    }

    zoomBy(center, deltaZoom) {
      const prevZoom = this.zoom

      this.zoom = this.zoom + deltaZoom

      this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
    }

    zoomIn(point) {
      this.zoomBy(point, 0.1)
    }

    zoomOut(point) {
      this.zoom > 0.25 && this.zoomBy(point, -0.1)
    }

    zoomPoint(center, rate, point) {
      const { x: cx, y: cy } = center
      const { x, y } = point

      const deltaX = (x - cx) * rate
      const deltaY = (y - cy) * rate

      const newPoint = {
        x: cx + deltaX,
        y: cy + deltaY
      }
      return newPoint
    }

    panBy(deltaX, deltaY) {
      const { x: centerX, y: centerY } = this.center
      this.center = {
        x: centerX + deltaX,
        y: centerY + deltaY
      }
    }

    getDeltaPointToPrevPanningPoint(point) {
      const { x, y } = point
      const { x: prevX, y: prevY } = this.prevZoomingPoint

      const deltaPoint = {
        x: x - prevX,
        y: y - prevY
      }
      return deltaPoint
    }


    startPan(event) {
      const point = {
        x: event.x - this.canvasLeft,
        y: event.y - this.canvasTop,
      }

      this.shouldPan = true

      this.prevZoomingPoint = point
    }

    panning(event) {
      const point = {
        x: event.x - this.canvasLeft,
        y: event.y - this.canvasTop,
      }

      const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
      const deltaY = this.getDeltaPointToPrevPanningPoint(point).y

      this.prevZoomingPoint = point

      this.panBy(deltaX, deltaY)
    }

    stopPan() {
      this.shouldPan = false
    }

    transformToInitial(point) {
      const { x, y } = point
      const { movement, zoom } = this
      const res = {
        x: (x - movement.x) / zoom,
        y: (y - movement.y) / zoom
      }
      return res
    }

    transform(point) {
      const { x, y } = point
      const { movement, zoom } = this
      const res = {
        x: x * zoom + movement.x,
        y: y * zoom + movement.y
      }
      return res
    }

    clearCanvas() {
      this.context.setTransform(1, 0, 0, 1, 0, 0)
      this.context.clearRect(
        0,
        0,
        viewPort.canvasWidth,
        viewPort.canvasHeight
      )
    }
  }

  class Interaction {
    constructor({
      canvas,
      viewPort,
      dragger
    }) {

      canvas.removeEventListener("mousewheel", mousewheelListener)
      canvas.addEventListener("mousewheel", mousewheelListener)

      canvas.removeEventListener("mousedown", mousedownListener)
      canvas.addEventListener("mousedown", mousedownListener)

      canvas.removeEventListener("mousemove", mousemoveListener)
      canvas.addEventListener("mousemove", mousemoveListener)

      canvas.removeEventListener("mouseup", mouseupListener)
      canvas.addEventListener("mouseup", mouseupListener)


      function mousewheelListener(event) {
        event.preventDefault()

        const point = {
          x: event.x - canvas.getBoundingClientRect().left,
          y: event.y - canvas.getBoundingClientRect().top,
        }

        const { deltaX, deltaY } = event

        if (isDecreasing()) {
          viewPort.zoomIn(point)
        }

        if (isIncreasing()) {
          viewPort.zoomOut(point)
        }

        function isIncreasing() {
          const res = deltaX > 0 || deltaY > 0
          return res
        }
        function isDecreasing() {
          const res = deltaX < 0 || deltaY < 0
          return res
        }

        render()

      }


      function mousedownListener(event) {
        viewPort.startPan(event)
      }

      function mousemoveListener(event) {
        viewPort.shouldPan && viewPort.panning(event)
        viewPort.shouldPan && render()
      }

      function mouseupListener(event) {
        viewPort.stopPan(event)
      }
    }

  }
  const canvas = document.getElementById("myCanvas")
  const viewPort = new ViewPort(canvas)
  const interaction = new Interaction({ viewPort, canvas })

  function render() {
    const { abs, max } = Math
    const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort

    viewPort.clearCanvas()
    ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)


    // Original codes are rewrote
    const { canvasWidth, canvasHeight } = viewPort

    const interval = 20
    const basicWidth = canvasWidth
    const basicHeight = canvasHeight

    const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
    const width = potentialWidth > basicWidth ? potentialWidth : basicWidth

    const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
    const height = potentialHeight > basicHeight ? potentialHeight : basicHeight

    drawXAxis()
    drawYAxis()
    drawOriginCoordinate()
    drawXCoordinates()
    drawYCoordinates()

    function drawXAxis() {
      const path = new Path2D

      path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
      path.lineTo(basicCenter.x + width / 2, basicHeight / 2)

      ctx.stroke(path)
    }

    function drawYAxis() {
      const path = new Path2D
      path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
      path.lineTo(basicWidth / 2, basicCenter.y + height / 2)

      ctx.stroke(path)
    }

    function drawOriginCoordinate() {
      ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
    }

    function drawXCoordinates() {
      for (let i = 1; i <= width / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` ${i} `, basicCenter.x + total, basicHeight / 2)
      }

      for (let i = 1; i <= width / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` -${i} `, basicCenter.x - total, basicHeight / 2)
      }
    }

    function drawYCoordinates() {
      for (let i = 1; i <= height / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` ${i} `, basicWidth / 2, basicCenter.y + total)
      }

      for (let i = 1; i <= height / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` -${i} `, basicWidth / 2, basicCenter.y - total)
      }
    }
  }

  render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>

1个回答

2

字体大小

对于字体大小,您需要选用一个与画布缩放比例成反比的字体大小。例如:

ctx.font = 12 / zoom + "px Arial";

当比例尺(zoom)为1时,字体大小为12。如果你放大了页面,使其伸展的比例加倍(zoom = 2),字体大小将变为6。由于字体大小是线性度量而不是面积度量,因此我们不需要在这里对缩放进行平方。

更新坐标轴

要更新显示的数字,以便它们按比例缩放,可以使用几种不同的方法。

例如,我们可以找出缩放的数量级(或者实际上它有多少位数字或者小数点),并基于该因素来显示坐标轴上的数字。例如,如果缩放为10,则我们将显示以1/10为增量的坐标轴数字。如果缩放为0.1,则我们将以1/0.1或10的增量显示坐标轴数字。

首先,让我们找出缩放的数量级:

const orderMagnitude = Math.pow(10,Math.floor(Math.log(zoom) / Math.LN10));

缩放值为1(起始值)将产生0的数量级。缩放值为10将产生1的数量级。

现在我们可以将数量级转换为一个四舍五入的基数10的数字:

const every = 1 / Math.pow(10,orderMagnitude);

在这里,我们以一个数量级为例,比如1,将其转换为1/10,1/10将成为坐标轴上显示的增量(您已经使用了变量名increment,所以我将其称为every,因为它代表每隔一段时间出现的坐标轴刻度)。将刻度间隔设置为相距1/10个单位是适当的,因为一个数量级表示10倍的缩放。

现在我们需要将这个应用到你代码的几个地方:

const inverval = 20 * every;  // scale the interval to reflect the density of ticks

当然,在设置轴时,例如:
 for (let i = 1; i <= width / 2 / interval; i++) {
    total = i * interval
    ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
  }

这是一个示例(缩小画面可以更快地展示它的效果):

class ViewPort {
    constructor(canvas) {
      this.canvas = canvas

      /**
        * Point used to calculate the change of every point's position on
        * canvas after view port is zoomed and panned
        */
      this.center = this.basicCenter

      this.zoom = 1

      this.shouldPan = false
      this.prevZoomingPoint = null
    }

    get canvasWidth() {
      return this.canvas.getBoundingClientRect().width
    }

    get canvasHeight() {
      return this.canvas.getBoundingClientRect().height
    }

    get canvasLeft() {
      return this.canvas.getBoundingClientRect().left
    }

    get canvasTop() {
      return this.canvas.getBoundingClientRect().top
    }

    get context() {
      return this.canvas.getContext('2d')
    }

    get basicCenter() {
      const { canvasWidth, canvasHeight } = this

      const point = {
        x: canvasWidth / 2,
        y: canvasHeight / 2
      }
      return point
    }

    get basicWidth() {
      const width = this.canvasWidth
      return width
    }

    get basicHeight() {
      const height = this.canvasHeight
      return height
    }

    get width() {
      const { basicWidth, zoom } = this
      const width = basicWidth * zoom
      return width
    }

    get height() {
      const { basicHeight, zoom } = this
      const height = basicHeight * zoom
      return height
    }

    get movement() {
      const { width, height, basicWidth, basicHeight } = this
      const { x: cx, y: cy } = this.center
      const { x: basicCX, y: basicCY } = this.basicCenter

      const deltaX = cx - basicCX - ((width - basicWidth) / 2)
      const deltaY = cy - basicCY - ((height - basicHeight) / 2)
      const res = {
        x: deltaX,
        y: deltaY
      }

      return res
    }

    get pan() {
      const { center, zoom, basicCenter } = this
      const res = {
        x: center.x - basicCenter.x,
        y: center.y - basicCenter.y
      }
      return res
    }

    zoomBy(center, deltaZoom) {
      const prevZoom = this.zoom

      this.zoom = this.zoom + deltaZoom

      this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
    }

    zoomIn(point) {
      this.zoomBy(point, 0.1)
    }

    zoomOut(point) {
      this.zoom > 0.25 && this.zoomBy(point, -0.1)
    }

    zoomPoint(center, rate, point) {
      const { x: cx, y: cy } = center
      const { x, y } = point

      const deltaX = (x - cx) * rate
      const deltaY = (y - cy) * rate

      const newPoint = {
        x: cx + deltaX,
        y: cy + deltaY
      }
      return newPoint
    }

    panBy(deltaX, deltaY) {
      const { x: centerX, y: centerY } = this.center
      this.center = {
        x: centerX + deltaX,
        y: centerY + deltaY
      }
    }

    getDeltaPointToPrevPanningPoint(point) {
      const { x, y } = point
      const { x: prevX, y: prevY } = this.prevZoomingPoint

      const deltaPoint = {
        x: x - prevX,
        y: y - prevY
      }
      return deltaPoint
    }


    startPan(event) {
      const point = {
        x: event.x - this.canvasLeft,
        y: event.y - this.canvasTop,
      }

      this.shouldPan = true

      this.prevZoomingPoint = point
    }

    panning(event) {
      const point = {
        x: event.x - this.canvasLeft,
        y: event.y - this.canvasTop,
      }

      const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
      const deltaY = this.getDeltaPointToPrevPanningPoint(point).y

      this.prevZoomingPoint = point

      this.panBy(deltaX, deltaY)
    }

    stopPan() {
      this.shouldPan = false
    }

    transformToInitial(point) {
      const { x, y } = point
      const { movement, zoom } = this
      const res = {
        x: (x - movement.x) / zoom,
        y: (y - movement.y) / zoom
      }
      return res
    }

    transform(point) {
      const { x, y } = point
      const { movement, zoom } = this
      const res = {
        x: x * zoom + movement.x,
        y: y * zoom + movement.y
      }
      return res
    }

    clearCanvas() {
      this.context.setTransform(1, 0, 0, 1, 0, 0)
      this.context.clearRect(
        0,
        0,
        viewPort.canvasWidth,
        viewPort.canvasHeight
      )
    }
  }

  class Interaction {
    constructor({
      canvas,
      viewPort,
      dragger
    }) {

      canvas.removeEventListener("mousewheel", mousewheelListener)
      canvas.addEventListener("mousewheel", mousewheelListener)

      canvas.removeEventListener("mousedown", mousedownListener)
      canvas.addEventListener("mousedown", mousedownListener)

      canvas.removeEventListener("mousemove", mousemoveListener)
      canvas.addEventListener("mousemove", mousemoveListener)

      canvas.removeEventListener("mouseup", mouseupListener)
      canvas.addEventListener("mouseup", mouseupListener)


      function mousewheelListener(event) {
        event.preventDefault()

        const point = {
          x: event.x - canvas.getBoundingClientRect().left,
          y: event.y - canvas.getBoundingClientRect().top,
        }

        const { deltaX, deltaY } = event

        if (isDecreasing()) {
          viewPort.zoomIn(point)
        }

        if (isIncreasing()) {
          viewPort.zoomOut(point)
        }

        function isIncreasing() {
          const res = deltaX > 0 || deltaY > 0
          return res
        }
        function isDecreasing() {
          const res = deltaX < 0 || deltaY < 0
          return res
        }

        render()

      }


      function mousedownListener(event) {
        viewPort.startPan(event)
      }

      function mousemoveListener(event) {
        viewPort.shouldPan && viewPort.panning(event)
        viewPort.shouldPan && render()
      }

      function mouseupListener(event) {
        viewPort.stopPan(event)
      }
    }

  }
  const canvas = document.getElementById("myCanvas")
  const viewPort = new ViewPort(canvas)
  const interaction = new Interaction({ viewPort, canvas })

  function render() {
    const { abs, max } = Math
    const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort

    viewPort.clearCanvas()
    ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)

 
 // modify font based on zoom:
 ctx.font = 12 / zoom + "px Arial";
 // modify number interval based on zoom:
 const orderMagnitude = Math.floor(Math.log(zoom) / Math.LN10);
 const every = 1 / Math.pow(10,orderMagnitude);

    // Original codes are rewrote
    const { canvasWidth, canvasHeight } = viewPort

    const interval = 20 * every; 
    const basicWidth = canvasWidth
    const basicHeight = canvasHeight

    const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
    const width = potentialWidth > basicWidth ? potentialWidth : basicWidth

    const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
    const height = potentialHeight > basicHeight ? potentialHeight : basicHeight

    drawXAxis()
    drawYAxis()
    drawOriginCoordinate()
    drawXCoordinates()
    drawYCoordinates()

    function drawXAxis() {
      const path = new Path2D

      path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
      path.lineTo(basicCenter.x + width / 2, basicHeight / 2)

      ctx.stroke(path)
    }

    function drawYAxis() {
      const path = new Path2D
      path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
      path.lineTo(basicWidth / 2, basicCenter.y + height / 2)

      ctx.stroke(path)
    }

    function drawOriginCoordinate() {
      ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
    }

    function drawXCoordinates() {
   for (let i = 1; i <= width / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
      }

      for (let i = 1; i <= width / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` -${i*every} `, basicCenter.x - total, basicHeight / 2)
      }
    }

    function drawYCoordinates() {
      for (let i = 1; i <= height / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` ${i*every} `, basicWidth / 2, basicCenter.y + total)
      }

      for (let i = 1; i <= height / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` -${i*every} `, basicWidth / 2, basicCenter.y - total)
      }
    }
  }

  render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>

优化坐标轴

目前的图表看起来还不错,但在zoom = 1时刻度线的刻度值并不是最理想的。也许我们可以通过偏移输入值来微调计算出的数量级:

const orderMagnitude = Math.pow(10,Math.floor(Math.log(zoom*1.5) / Math.LN10));

这将为不同轴的刻度产生稍微更好的间隔阈值。
进一步改进:
我们可以使用2或5作为中间值,而不是让每个原点旁边的刻度从1 x 10^n开始,因为当缩放比例变化了10倍时重置刻度并不是最理想的。
一种可能的解决方案是:随着缩放比例因子相对于给定数量级的增加,我们减少刻度之间的间隔(每次减少every)。
// Modify how every often we want to show an axis tick:
var every;
if (zoom/Math.pow(10,orderMagnitude) > 4) {
    every = 1 / Math.pow(10,orderMagnitude) * 0.2;
}
else if (zoom/Math.pow(10,orderMagnitude) > 2) {
    every = 1 / Math.pow(10,orderMagnitude) * 0.5;
}
else {
    every = 1 / Math.pow(10,orderMagnitude);
}

这给我们带来了:

class ViewPort {
    constructor(canvas) {
      this.canvas = canvas

      /**
        * Point used to calculate the change of every point's position on
        * canvas after view port is zoomed and panned
        */
      this.center = this.basicCenter

      this.zoom = 1

      this.shouldPan = false
      this.prevZoomingPoint = null
    }

    get canvasWidth() {
      return this.canvas.getBoundingClientRect().width
    }

    get canvasHeight() {
      return this.canvas.getBoundingClientRect().height
    }

    get canvasLeft() {
      return this.canvas.getBoundingClientRect().left
    }

    get canvasTop() {
      return this.canvas.getBoundingClientRect().top
    }

    get context() {
      return this.canvas.getContext('2d')
    }

    get basicCenter() {
      const { canvasWidth, canvasHeight } = this

      const point = {
        x: canvasWidth / 2,
        y: canvasHeight / 2
      }
      return point
    }

    get basicWidth() {
      const width = this.canvasWidth
      return width
    }

    get basicHeight() {
      const height = this.canvasHeight
      return height
    }

    get width() {
      const { basicWidth, zoom } = this
      const width = basicWidth * zoom
      return width
    }

    get height() {
      const { basicHeight, zoom } = this
      const height = basicHeight * zoom
      return height
    }

    get movement() {
      const { width, height, basicWidth, basicHeight } = this
      const { x: cx, y: cy } = this.center
      const { x: basicCX, y: basicCY } = this.basicCenter

      const deltaX = cx - basicCX - ((width - basicWidth) / 2)
      const deltaY = cy - basicCY - ((height - basicHeight) / 2)
      const res = {
        x: deltaX,
        y: deltaY
      }

      return res
    }

    get pan() {
      const { center, zoom, basicCenter } = this
      const res = {
        x: center.x - basicCenter.x,
        y: center.y - basicCenter.y
      }
      return res
    }

    zoomBy(center, deltaZoom) {
      const prevZoom = this.zoom

      this.zoom = this.zoom + deltaZoom

      this.center = this.zoomPoint(center, this.zoom / prevZoom, this.center)
    }

    zoomIn(point) {
      this.zoomBy(point, 0.1)
    }

    zoomOut(point) {
      this.zoom > 0.25 && this.zoomBy(point, -0.1)
    }

    zoomPoint(center, rate, point) {
      const { x: cx, y: cy } = center
      const { x, y } = point

      const deltaX = (x - cx) * rate
      const deltaY = (y - cy) * rate

      const newPoint = {
        x: cx + deltaX,
        y: cy + deltaY
      }
      return newPoint
    }

    panBy(deltaX, deltaY) {
      const { x: centerX, y: centerY } = this.center
      this.center = {
        x: centerX + deltaX,
        y: centerY + deltaY
      }
    }

    getDeltaPointToPrevPanningPoint(point) {
      const { x, y } = point
      const { x: prevX, y: prevY } = this.prevZoomingPoint

      const deltaPoint = {
        x: x - prevX,
        y: y - prevY
      }
      return deltaPoint
    }


    startPan(event) {
      const point = {
        x: event.x - this.canvasLeft,
        y: event.y - this.canvasTop,
      }

      this.shouldPan = true

      this.prevZoomingPoint = point
    }

    panning(event) {
      const point = {
        x: event.x - this.canvasLeft,
        y: event.y - this.canvasTop,
      }

      const deltaX = this.getDeltaPointToPrevPanningPoint(point).x
      const deltaY = this.getDeltaPointToPrevPanningPoint(point).y

      this.prevZoomingPoint = point

      this.panBy(deltaX, deltaY)
    }

    stopPan() {
      this.shouldPan = false
    }

    transformToInitial(point) {
      const { x, y } = point
      const { movement, zoom } = this
      const res = {
        x: (x - movement.x) / zoom,
        y: (y - movement.y) / zoom
      }
      return res
    }

    transform(point) {
      const { x, y } = point
      const { movement, zoom } = this
      const res = {
        x: x * zoom + movement.x,
        y: y * zoom + movement.y
      }
      return res
    }

    clearCanvas() {
      this.context.setTransform(1, 0, 0, 1, 0, 0)
      this.context.clearRect(
        0,
        0,
        viewPort.canvasWidth,
        viewPort.canvasHeight
      )
    }
  }

  class Interaction {
    constructor({
      canvas,
      viewPort,
      dragger
    }) {

      canvas.removeEventListener("mousewheel", mousewheelListener)
      canvas.addEventListener("mousewheel", mousewheelListener)

      canvas.removeEventListener("mousedown", mousedownListener)
      canvas.addEventListener("mousedown", mousedownListener)

      canvas.removeEventListener("mousemove", mousemoveListener)
      canvas.addEventListener("mousemove", mousemoveListener)

      canvas.removeEventListener("mouseup", mouseupListener)
      canvas.addEventListener("mouseup", mouseupListener)


      function mousewheelListener(event) {
        event.preventDefault()

        const point = {
          x: event.x - canvas.getBoundingClientRect().left,
          y: event.y - canvas.getBoundingClientRect().top,
        }

        const { deltaX, deltaY } = event

        if (isDecreasing()) {
          viewPort.zoomIn(point)
        }

        if (isIncreasing()) {
          viewPort.zoomOut(point)
        }

        function isIncreasing() {
          const res = deltaX > 0 || deltaY > 0
          return res
        }
        function isDecreasing() {
          const res = deltaX < 0 || deltaY < 0
          return res
        }

        render()

      }


      function mousedownListener(event) {
        viewPort.startPan(event)
      }

      function mousemoveListener(event) {
        viewPort.shouldPan && viewPort.panning(event)
        viewPort.shouldPan && render()
      }

      function mouseupListener(event) {
        viewPort.stopPan(event)
      }
    }

  }
  const canvas = document.getElementById("myCanvas")
  const viewPort = new ViewPort(canvas)
  const interaction = new Interaction({ viewPort, canvas })

  function render() {
    const { abs, max } = Math
    const { zoom, movement, context: ctx, pan, center, basicCenter } = viewPort

    viewPort.clearCanvas()
    ctx.setTransform(zoom, 0, 0, zoom, movement.x, movement.y)

 
 // modify font based on zoom:
 ctx.font = 12 / zoom + "px Arial";
 // modify number interval based on zoom:
 const orderMagnitude = Math.floor(Math.log(zoom*1.5) / Math.LN10);
 
 // Modify how every often we want to show an axis tick:
 var every;
 if (zoom/Math.pow(10,orderMagnitude) > 4) {
  every = 1 / Math.pow(10,orderMagnitude) * 0.2;
 }
 else if (zoom/Math.pow(10,orderMagnitude) > 2) {
  every = 1 / Math.pow(10,orderMagnitude) * 0.5;
 }
 else {
  every = 1 / Math.pow(10,orderMagnitude);
 }
 
    // Original codes are rewrote
    const { canvasWidth, canvasHeight } = viewPort

    const interval = 30 * every; 
    const basicWidth = canvasWidth
    const basicHeight = canvasHeight

    const potentialWidth = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).x - basicCenter.x), abs(viewPort.transformToInitial({ x: basicWidth, y: 0 }).x - basicCenter.x))
    const width = potentialWidth > basicWidth ? potentialWidth : basicWidth

    const potentialHeight = 2 * max(abs(viewPort.transformToInitial({ x: 0, y: 0 }).y - basicCenter.y), abs(viewPort.transformToInitial({ x: 0, y: basicHeight }).y - basicCenter.y))
    const height = potentialHeight > basicHeight ? potentialHeight : basicHeight

    drawXAxis()
    drawYAxis()
    drawOriginCoordinate()
    drawXCoordinates()
    drawYCoordinates()

    function drawXAxis() {
      const path = new Path2D

      path.moveTo(basicCenter.x - width / 2, basicHeight / 2)
      path.lineTo(basicCenter.x + width / 2, basicHeight / 2)

      ctx.stroke(path)
    }

    function drawYAxis() {
      const path = new Path2D
      path.moveTo(basicWidth / 2, basicCenter.y - height / 2)
      path.lineTo(basicWidth / 2, basicCenter.y + height / 2)

      ctx.stroke(path)
    }

    function drawOriginCoordinate() {
      ctx.fillText(`O`, basicCenter.x + 5, basicCenter.y - 5)
    }

    function drawXCoordinates() {
   for (let i = 1; i <= width / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` ${i*every} `, basicCenter.x + total, basicHeight / 2)
      }

      for (let i = 1; i <= width / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` -${i*every} `, basicCenter.x - total, basicHeight / 2)
      }
    }

    function drawYCoordinates() {
      for (let i = 1; i <= height / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` ${i*every} `, basicWidth / 2, basicCenter.y + total)
      }
   
   for (let i = 1; i <= height / 2 / interval; i++) {
        total = i * interval
        ctx.fillText(` -${i*every} `, basicWidth / 2, basicCenter.y - total)
      }
    }
 
 

 
  }

  render()
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>

进一步改进

我还没有涉及数字格式,但可以注意到在缩放时会出现一些浮点数问题。此外,随着我们放大,轴条的宽度增加,缩小时则减小,这会影响文本的定位。


你好,非常感谢您的出色答案。这是我见过的最好的答案。再次感谢。我尝试在我的智能手机上使用canva,但我无法使用触摸屏幕上的功能,例如缩放或移动坐标系。您能帮我解决这个问题吗? - Tareq Jami
@TareqJami,没问题,很高兴能帮忙,并感谢你的夸奖。 - Andrew Reid
@TareqJami,抱歉我错过了关于手机缩放的部分 - 当时有点分心。最好作为一个新问题,这样更多的人会看到它,我不是最擅长手机手势的人。 - Andrew Reid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接