Search code examples
javascriptcanvashtml5-canvas

Draw text at center of polygons


I get an array of polygons from a database. Each shape may be a triangle, a rectangle, a square, or any polygon.

I want to draw text at the center of each polygon. Font size must be dynamic according to the size of each polygon. Text color should match the line color.

Example from database:

colored polygons

Here is my code:

var polygons = [
  {
    text: "ROI",    color: "#00ff00",
    jointLength: 5,    lineWidth: 3,
    X: [890, 893, 409, 21, 27],    Y: [658, 205, 199, 556, 659],
  },  {
    text: "Lane 3",    color: "#ff0000",
    jointLength: 4,    lineWidth: 3,
    X: [915, 911, 643, 879],    Y: [5, 682, 683, 2],
  },  {
    text: "Lane 4",    color: "#ff0000",
    jointLength: 4,    lineWidth: 3,
    X: [888, 656, 170, 701],    Y: [2, 680, 682, 1],
  },  {
    text: "Lane 5",    color: "#ff0000",
    jointLength: 5,    lineWidth: 3,
    X: [712, 182, 4, 4, 590],    Y: [1, 681, 682, 532, 1],
  },  {
    text: "Speed",    color: "#0000ff",
    jointLength: 4,    lineWidth: 3,
    X: [290, 911, 873, 5],    Y: [367, 357, 668, 664],
  }
];

polygons.forEach((polygon) => {
  const ctx = document.getElementById("canvas").getContext("2d");
  ctx.strokeStyle = polygon.color;
  ctx.lineWidth = polygon.lineWidth;
  ctx.beginPath();
  ctx.moveTo(polygon.X[0], polygon.Y[0]);
  for (let i = 1; i < polygon.jointLength; i++) {
    ctx.lineTo(polygon.X[i], polygon.Y[i]);
  }
  ctx.closePath();
  ctx.stroke();
});
<canvas id="canvas" width=999 height=999></canvas>


Solution

  • Explanation of main logic:

    • The center of polygon I calculated by the formula of the arithmetic mean
    • The size of font I calculated by getting the width of text with font-size = 300 (but you can change the first check size as you want) and then check if text with is more than the smallest distance between 2 nearest dots (I think that this is good limit if text will be at the center of polygon). If yes then I start to find correct font-size with binary search algorithm

    Because of this logic the text in second polygon is smaller than it can be because we have 2 dots at the top which are very close to each other

    There is a code (open in full page for better visibility):

    const polygons = [
      {
        text: "ROI",
        color: "red",
        jointLength: 5,
        lineWidth: 3,
        X: [890, 893, 409, 21, 27],
        Y: [658, 205, 199, 556, 659],
      },
      {
        text: "Lane 3",
        color: "blue",
        jointLength: 4,
        lineWidth: 3,
        X: [915, 911, 643, 879],
        Y: [5, 682, 683, 2],
      },
      {
        text: "Lane 4",
        color: "green",
        jointLength: 4,
        lineWidth: 3,
        X: [888, 656, 170, 701],
        Y: [2, 680, 682, 1],
      },
      {
        text: "Lane 5",
        color: "orange",
        jointLength: 5,
        lineWidth: 3,
        X: [712, 182, 4, 4, 590],
        Y: [1, 681, 682, 532, 1],
      },
      {
        text: "Speed",
        color: "purple",
        jointLength: 4,
        lineWidth: 3,
        X: [290, 911, 873, 5],
        Y: [367, 357, 668, 664],
      },
    ];
    
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext("2d");
    canvas.width = 1000;
    canvas.height = 1000;
    
    class Polygon {
      #ctx;
      #dots = [];
      #text;
      #color;
      #lineWidth;
      #dotsCount;
      
      constructor(ctx, data) {
        this.#ctx = ctx;
        this.#text = data.text;
        this.#color = data.color;
        this.#lineWidth = data.lineWidth;
        this.#dotsCount = data.jointLength;
        
        for (let i = 0; i < this.#dotsCount; ++ i) {
          this.#dots.push({x: data.X[i], y: data.Y[i]})
        }
      }
      
      #getCenterCoords() {
        const x = this.#dots.reduce((sum, dot) => sum += dot.x, 0) / this.#dotsCount;
        const y = this.#dots.reduce((sum, dot) => sum += dot.y, 0) / this.#dotsCount;
        
        return {x, y};
      }
      
      #distance = (dot1, dot2) => Math.sqrt((dot1.x - dot2.x) ** 2 + (dot1.y - dot2.y) ** 2);
      
      #getMinimalDistanceBetweenDots() {
        let minDist = Infinity;
        
        for (let i = 0; i < this.#dotsCount; ++i) {
          const dot1 = this.#dots[i];
          
          for (let j = i + 1; j < this.#dotsCount; ++j) {
            const dot2 = this.#dots[j];
            const dist = this.#distance(dot1, dot2);
            
            if (dist < minDist) minDist = dist;
          }
        }
        
        return minDist;
      }
      
      #getTextSize() {
        const minAvailableWidth = this.#getMinimalDistanceBetweenDots();
        
        let rightBound = 300;
        let leftBound = 0;
        let fontSize = rightBound;
    
        while (rightBound - leftBound > 1) {
        
          fontSize = Math.round((leftBound + rightBound) / 2);
          this.#ctx.font = `${fontSize}px verdana`;
          const textSize = this.#ctx.measureText(this.#text).width;
          
          if (textSize > minAvailableWidth) {
            rightBound = fontSize;
            continue;
          }
          
          if (textSize < minAvailableWidth) {
            leftBound = fontSize;
            continue;
          }
          
          if (textSize === minAvailableWidth) {
            break;
          }
        }
        
        return fontSize;
      }
      
      draw() {
        const path = new Path2D();
        const firstDot = this.#dots[0];
        const center = this.#getCenterCoords();
      
        this.#dots.forEach(dot => path.lineTo(dot.x, dot.y));
    
        path.lineTo(firstDot.x, firstDot.y);
    
        this.#ctx.strokeStyle = this.#color;
        this.#ctx.lineWidth = this.#lineWidth;
        this.#ctx.lineCap = 'round';
        this.#ctx.lineJoin = 'round';
        this.#ctx.stroke(path);
        
        this.#ctx.font = `${this.#getTextSize()}px verdana`;
        this.#ctx.fillStyle = this.#color;
        this.#ctx.textAlign = 'center'; 
        this.#ctx.fillText(this.#text, center.x, center.y);
      }
    }
    
    polygons.forEach((polygon) => new Polygon(ctx, polygon).draw());
    <canvas id="canvas"></canvas>