Search code examples
javascripthtml5-canvas

Canvas not showing some paths


Why are my paths sometimes not showing up when calculated by my internal functions from variables like %?

When developing simple graphic tool to create fillable templates which I can convert to images, I noticed that sometimes my drawn paths were disappearing depending on their positioning. This could be fixed by adding 0.02px or Number.ESPSILON to some points, but this is not really a solution. Why is that happening?

Code in question:

  // Starts with setting canvas
  async setCanvas() {
    this.$self.get.loading.style.display = '';
    this.canvas = this.$self.get.canvas;
    this.canvas.setAttribute('width', this.canvas.offsetWidth * this.$.scale)
    this.canvas.setAttribute('height', this.canvas.offsetHeight * this.$.scale)
    this.ctx = this.canvas.getContext("2d");
    this.picture.x = this.decimal(((this.canvas.offsetWidth - this.$.width) / 2)*this.$.scale);
    this.picture.y = this.decimal(((this.canvas.offsetHeight - this.$.height) / 2)*this.$.scale);
    this.drawCanvas();
    // Then we draw on it
    await this.draw();
    this.loaderDebouncer();
  }

  [...]

  async draw() {
    for(let i=0; i < this.$.layout.length; i++) {
      const layout = this.$.layout[i];
      Object.values(layout.inputs || {}).forEach(input => {
        // this should not be relevant as is just integration of Form framework
        if (!input.object) {
          input.object = FormBuilder.sGenerateFieldItem(input.value || null);
          this.setMain(input.value, layout, input);
        }
      });
      await this.drawEntity(layout);
    }
  }
  
 [...]

 async drawEntity(entity) {
    const types = {
      image: e => this.drawImage(e),
      rect: e => this.drawRect(e),
      text: e => this.drawText(e),
      polygon: e => this.drawPolygon(e), // <- draws the path
      default: e => console.error('Not recognized type', e.type),
    };

    await (types[entity.type] || types.default)(entity);
  }

  // !! The relevant method!
  async drawPolygon(entity) {
    const data = entity.data,
      path = await this.retrieveValue(data.path || [], entity),  
      { width, height, left, top } = this.calculateSizeAndPos(entity),
      x = this.decimal(this.picture.x + left),
      y = this.decimal(this.picture.y + top);

    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.moveTo(x, y);

    path.forEach(point => {
      this.polygonFunction(x, y, point[0])(...point.slice(1));
    });

    this.ctx.fillStyle = this.fill(data.fill) || this.ctx.fillStyle;
    this.ctx.fill();
    this.ctx.closePath();

    if (entity.custom) {
      entity.custom(this.ctx);
    }

    if (entity.outline) {
      const outline = entity.outline;
      this.polygonFunction(x, y, 'stroke')(this.thickness(outline.thickness || 5), this.fill(outline.fill || '#000'));
    }

    this.ctx.restore();
  }

  polygonFunction(x, y, type) {
    const fns = {
      line: (lineX, lineY,) => {
        lineX = this.decimal(x + this.calc(lineX, 'x')), lineY = this.decimal(y + this.calc(lineY, 'y'));
        this.ctx.lineTo(lineX, lineY)
      },
      curve: (cp1x, cp1y, cp2x, cp2y, curveX, curveY) => {
        cp1x = this.decimal(x + this.calc(cp1x, 'x'));
        cp1y = this.decimal(y + this.calc(cp1y, 'y'));
        cp2x = this.decimal(x + this.calc(cp2x, 'x'));
        cp2y = this.decimal(y + this.calc(cp2y, 'y'));
        curveX = this.decimal(x + this.calc(curveX, 'x'));
        curveY = this.decimal(y + this.calc(curveY, 'y'));

        this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, curveX, curveY);
      },
      stroke: (thickness, fill) => {
        this.ctx.save();
        this.ctx.strokeStyle = fill || '#000';
        this.ctx.lineWidth =  this.thickness(thickness, 5);
        this.ctx.lineJoin = "round";
        this.ctx.miterLimit = 2;
        this.ctx.stroke();
        this.ctx.restore();
      },
      default: () => fns.line(...arguments),
    };

    return fns[type] || fns.default
  }

  decimal(number, precision = 2) {
    return +number.toFixed(precision);
  }

  retrieveValue(value, entity) {
    if (typeof value == 'function') {
      return value(entity, this.$.indexes, this);
    }

    return value;
  }

  calculateSizeAndPos(entity) {
    return {
      width: this.calc(entity.size?.w, 'x'),
      height: this.calc(entity.size?.h, 'y'),
      left: this.calc(entity.pos?.x, 'x'),
      top: this.calc(entity.pos?.y, 'y')
    }
  }

Settings used for the disappearing path:

      {
        id: 'arrow',
        label: 'Rune Position',
        editable: true,
        type: 'polygon',
        pos: {
          x: '42.5%',
          y: '60%',
        },
        data: {
          path: layer => {
            if (layer.inputs.position.value == 0) {
              layer.data.fill = '#FFF';
              layer.outline.fill = '#000';

              return [
                ['line', '1.5%', '2.5w%'],
                ['line', '-1.5%', '2.5w%'],
              ];
            }

            layer.data.fill = '#000';
            layer.outline.fill = '#FFF';

            return [
              ['line', '1.5%', '0'],
              ['line', '0', '2.5w%'],
              ['line', '-1.5%', '0'],
            ];
          },
          fill: '#FFF'
        },
        outline: {
          fill: '#000',
          thickness: .5,
        },
        inputs: {
          position: {
            type: 'select',
            label: 'Rune Position',
            options: [
              ['Top', '0'],
              ['Bottom', '1'],
            ],
            value: '0',
            inherited: null,
          }
        }
      }

Solution

  • I'm closing this one. It looks like a bug on Firefox 114.0.1 (64-bit) Linux (Ubuntu 20) as opening the page in incognito tag or another browser (Chrome) fixes it for that session.