Tareq Jami
Tareq Jami

Reputation: 99

fixed font size html canvas, how to make it change while zooming in/out

I've made a cartesian coordinates system before, with html canvas. A user helped me to add the zoom function using the mouse.

But I've got one problem. The font size of the axis numbers are fixed, so while zooming out, the font size is becoming smaller too.

I want a fixed font size but a changeable intervall between numbers.

so if you zoom in for example, you see the numbers on the x-axis 0, 1, 2, 3, 4, 5

but once you zoom out it should be 0, 5, 10, 15

like geogebra https://www.geogebra.org/classic

I need to make my own coordinate system and can't use applets or embeded codes for the project.

The codes what I've got till now

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>

Upvotes: 4

Views: 1115

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38201

Font Size

For font size, you want a font size that scales inversely to the zoom value of the canvas. Say:

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

Where 12 is the font size when the scale (zoom) is 1. If you zoom in so everything is twice as stretched, (zoom = 2), the font size will be 6. As font size is a linear not areal measure so we don't need to square the zoom here.

Updating Axes

To update the numbers displayed so that they are scaled appropriately a few different approaches could be used.

As a simple example, we could find out the order of magnitude of the zoom (or essentially how many digits it is or how many decimal spots it has) and scale the numbers shown based on this factor. For example, if the zoom is 10, then we would show axis numbers in 1/10 increments. If the zoom is 0.1, then we would show axis numbers in increments of 1/0.1 or 10.

First let's find out the order of magnitude of the zoom:

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

A zoom value of 1 (start value) produces an order of magnitude of 0. A zoom value of 10 produces an order of magnitude of 1.

Now we can take that order of magnitude and convert into a round base 10 number:

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

Here we take an order of magnitude, such as 1 and convert it into 1/10, 1/10 will be the increment shown on the axes (you've already used the variable name increment, so I've called it every, as it represents an axis tick every so often). This spacing of ticks 1/10 of a unit apart is appropriate as an order of magnitude represents a 10x zoom.

Now we need to apply this to a few spots in your code:

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

And of course when you set up the axes, eg:

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

Here's an example so far (zooming out shows this in action much faster):

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>

Refining the Axes

This is ok, but the threshold for ticks at zoom = 1 isn't ideally placed. Perhaps we can modify the calculated order of magnitude a bit by offsetting the input value:

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

This will produce a slightly better spaced threshold for different axes' ticks.

Further Refinement

Rather than having each tick next to the origin start at 1 x 10^n we could use 2 or 5 for intermediate values, as resetting the ticks only when the zoom scale changes by a factor of 10 isn't most ideal.

One possible solution is that as a zoom scale factor increases relative to a given order of magnitude we decrease the interval between ticks (decrease 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);
}

This gives us:

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>

Further Refinements

I haven't touched number formatting, but you can see some floating point issues when zooming in. Also, the axes' bars widths grow as we zoom in and shrink as we zoom out, this affects text positioning.

Upvotes: 3

Related Questions