Drawing a wavy line in FabricJS

后端 未结 2 748
囚心锁ツ
囚心锁ツ 2021-01-01 02:35

I\'m using FabricJS to create a canvas for drawing specific lines and shapes. One of the lines is a wavy line with an arrow similar to this:

I\'ve successfu

2条回答
  •  情歌与酒
    2021-01-01 03:05

    Results

    I'm not really an expert, but I attempted to implement wavy lines all by myself.

    That's the result:

    Coding

    I used the fabric.Group class to group lines that make up our wavy line.

    const WavyLineWithArrow = fabric.util.createClass(fabric.Group, {
        /* ... */
    };
    

    The lines are removed and added to the object after each change:

    this.forEachObject(function(o) {
        this.remove(o);
    }, this);
    
    for(var i=1;i

    Arrow at the end of a line is also an object:

      this.add(new fabric.Polyline([
        {x: len/2, y: -arrowSize/2},
        {x: len/2 + arrowSize/2, y: 0},
        {x: len/2, y: arrowSize/2},
        {x: len/2, y: -arrowSize/2}
      ], arrOptions));
    

    All the hard task was the calculation of function values, scalling etc. but it's just boring geometry.

    Disclaimer

    I tested my wavy line implementation and it seems to work nicely even if you support other function (that is not a sine).

    Only one problem I see that's in your example you rendered lines from corner to corner.

    It's not a big deal to rotate the wavy line, but that's all the differences from the ideal solution that I noticed.

    Fancy types of arrows

    I made the following nice arrows:

    // Default: sine
    null
    
    // Custom: tangens
    [
        function(x) { return Math.max(-10, Math.min(Math.tan(x/2) / 3, 10)); },
        4 * Math.PI
    ]
    
    // Custom: Triangle function
    [
        function(x) {
          let g = x % 6;
          if(g<=3) return g*5;
          if(g>3) return (6-g)*5;
        },
        6
    ]
    
    // Custom: Square function
    [
        function(x) {
          let g = x % 6;
          if(g<=3) return 15;
          if(g>3) return -15;
        },
        6
    ]
    

    Full example

    Below I attach my snipped with working wavy lines.
    You can also view that snippet on codepen.io

    var fabricCanvas = this.__canvas = new fabric.Canvas('c');
    fabricCanvas.setHeight(300);
    fabricCanvas.setWidth(600);
    
    const LineWithArrow = fabric.util.createClass(fabric.Line, {
      type: 'line_with_arrow',
    
      initialize(element, options) {
        options || (options = {});
        this.callSuper('initialize', element, options);
    
        // Set default options
        this.set({
          hasBorders: false,
          hasControls: false,
        });
      },
    
      _render(ctx) {
        this.callSuper('_render', ctx);
        ctx.save();
        const xDiff = this.x2 - this.x1;
        const yDiff = this.y2 - this.y1;
        const angle = Math.atan2(yDiff, xDiff);
        ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
        ctx.rotate(angle);
        ctx.beginPath();
        // Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
        ctx.moveTo(5, 0);
        ctx.lineTo(-5, 5);
        ctx.lineTo(-5, -5);
        ctx.closePath();
        ctx.fillStyle = this.stroke;
        ctx.fill();
        ctx.restore();
      },
    
      toObject() {
        return fabric.util.object.extend(this.callSuper('toObject'), {
          customProps: this.customProps,
        });
      },
    });
    
    /*
     * WavyLineWithArrow
     *
     * It has four coords as normal arrow: x1, x2, y1, y2
     * Plus you can provide custom function for arrow.funct attribute
     *
     * It can be plain javascript function:
     *     arrow.funct = function(x) { return x/10; }
     *   Then the result way be disturbing (line generated by function may lay not in a valid place)
     *
     * For that purpose you do:
     *     arrow.funct = [ function(x) { / periodic function / }, period ];
     *   This will allow the object to caluclate nicely ending arrow.
     *   The function don't have to be periodic (in the mathematical sense).
     *   You just shall meet the assumption:
     *
     *      f(n*T) = 0 for any n = 0, 1, 2, 3...
     *   
     *   And everything will work nicely.
     *
     */
    const WavyLineWithArrow = fabric.util.createClass(fabric.Group, {
      type: 'wavy_line_with_arrow',
      
      initialize(points, options) {
        options || (options = {});
        
        // Set initial dimensions of arrow
        this.coord_x1 = points[0];
        this.coord_y1 = points[1];
        this.coord_x2 = points[2];
        this.coord_y2 = points[3];
        this.arrowSize = options.arrowSize || 10;
        
        const selfOptions = fabric.util.object.clone(options);
        selfOptions.top =  this.coord_y1;
        selfOptions.left = this.coord_x1;
        
        // Set initial dimensions of arrow
        this.set({
          width: this.coord_x2 - this.coord_x1,
          height: this.coord_y2 - this.coord_y1,
          top: this.coord_y1,
          left: this.coord_x1
        });
        this.setCoords();
        
        /*
         * Set default values
         */
        
        this._funct_ = selfOptions.funct;
        if(this._funct_ === null || this._funct_ === undefined) {
            this._funct_ = function(x) {
                return Math.sin(x) * 10;
            };
        }
        
        this.period = selfOptions.period;
        if(!this.period) {
            this.period = 1;
        }
        
        // Function for updating coords
        this.updateCoords = () => {
            this.set({
                width: this.coord_x2 - this.coord_x1,
                height: this.coord_y2 - this.coord_y1,
                top: this.coord_y1,
                left: this.coord_x1
            });
            this.setCoords();
        };
        
        /*
         * This section defines hacky getters/setters
         * which enable the object to self update when you do object.funct = function(){ ... } etc.
         */
        
        Object.defineProperty(this, 'x1', {
            set: (x1) => {
                this.coord_x1 = x1;
                this.updateCoords();
                this.updateInternalPointsData();
                this.dirty = true;
            },
            get: () => {
                return this.coord_x1;
            }
        });
        
        Object.defineProperty(this, 'x2', {
            set: (x2) => {
                this.coord_x2 = x2;
                this.updateCoords();
                this.updateInternalPointsData();
                this.dirty = true;
            },
            get: () => {
                return this.coord_x2;
            }
        });
        
        Object.defineProperty(this, 'y1', {
            set: (y1) => {
                this.coord_y1 = y1;
                this.updateCoords();
                this.updateInternalPointsData();
                this.dirty = true;
            },
            get: () => {
                return this.coord_y1;
            }
        });
        
        Object.defineProperty(this, 'y2', {
            set: (y2) => {
                this.coord_y2 = y2;
                this.updateCoords();
                this.updateInternalPointsData();
                this.dirty = true;
            },
            get: () => {
                return this.coord_y2;
            }
        });
        
        Object.defineProperty(this, 'funct', {
            set: (value) => {
                this._funct_ = value;
                if(value) {
                    this.period = 1;
                    if(value[0]) {
                        this._funct_ = value[0];
                    }
                    if(value[1]) {
                        this.period = value[1] || 1;
                    }
                }
                this.updateInternalPointsData();
                this.dirty = true;
            },
            get: () => {
                return this._funct_;
            }
        });
        
        /*
         * This function generates list of points that are placed inside the Group
         */
        this.updateInternalPointsData = () => {
          
          // Head size is a length of strainght line at the end near arrow
          const headSize = 20;
          // Basic scale factor is a scale factor for the provided "waving" function
          const basicScaleFactorX = 0.2;
          // Scaling factor for y axis
          const scaleFactorY = 1.0;
          // The size of the pointy arrow at the end
          const arrowSize = this.arrowSize || 10;
          
          /*
           * Synchronize coordinates
           */
          this.coord_x1 = this.left;
          this.coord_y1 = this.top;
          this.coord_x2 = this.coord_x1 + this.width;
          this.coord_y2 = this.coord_y1 + this.height;
          
          // Length of the line
          const len = this.width;
          // Generated points array
          const polyPoints = [];
          
          /*
           * Calculate period rescale factor
           * This is additional factor for scalling X that ensures we have only full periods in the line length
           */
          let periodRescaleFactor = this.period/basicScaleFactorX * Math.floor((len-headSize) / (this.period/basicScaleFactorX)) / (len-headSize);
          if(periodRescaleFactor === undefined || periodRescaleFactor < 0.001) {
              periodRescaleFactor = 1;
          }
          
          // Calulate final x scale factor
          const scaleFactorX = basicScaleFactorX * periodRescaleFactor;
          
          // Use default function?
          if(this._funct_ === null || this._funct_ === undefined) {
            this._funct_ = function(x) {
                return Math.sin(x) * 10;
            };
            this.period = Math.PI * 2;
          }
          
          // Use default period?
          if(!this.period) {
              this.period = 1;
          }
          
          // Generate poins:
          //  from [-len/2, 0] up to [len/2, 0]
          var step = 0.5;
          for(var x=0; x (
      new LineWithArrow(points, {
        customProps: item,
        strokeWidth: 2,
        stroke: color,
      })
    )
    
    drawWavyLineWithArrow = (item, points, color, funct) => (
      new WavyLineWithArrow(points, {
        customProps: item,
        strokeWidth: 2,
        stroke: color,
        funct: funct
      })
    )
    
    selectLine = (item, points) => {
      switch (item.type) {
        case 'line_with_arrow':
          return this.drawLineWithArrow(item, points, fabric.Color.fromRgb("rgb(255,0,0)"));
    
        case 'wavy_line_with_arrow':
          return this.drawWavyLineWithArrow(item, points, fabric.Color.fromRgb("rgb(255,0,0)"));
        // no default
      }
      return null;
    }
    
    let line;
    let isDown;
    
    let typesOfLinesIter = -1;
    const typesOfLines = [
        // Default: sine
        null,
        // Custom: tangens with period marked as 4PI
        [
            function(x) { return Math.max(-10, Math.min(Math.tan(x/2) / 3, 10)); },
            4 * Math.PI
        ]
    ];
    
    fabricCanvas.on('mouse:down', (options) => {
      isDown = true;
      once = true;
      
      const pointer = fabricCanvas.getPointer(options.e);
      const points = [pointer.x, pointer.y, pointer.x, pointer.y];
      
      const item = {
        type: 'wavy_line_with_arrow'
      };
      
      line = this.selectLine(item, points);
     
      ++typesOfLinesIter;
      typesOfLinesIter %= typesOfLines.length;
      
      // Customize render function of the line
      line.set({ funct: typesOfLines[typesOfLinesIter] });
      
      fabricCanvas
        .add(line)
        .setActiveObject(line)
        .renderAll();
    });
    
    fabricCanvas.on('mouse:move', (options) => {
      if (!isDown) return;
      const pointer = fabricCanvas.getPointer(options.e);
      line.set({ x2: pointer.x, y2: pointer.y });
      fabricCanvas.renderAll();
    });
    
    fabricCanvas.on('mouse:up', () => {
      isDown = false;
      line.setCoords();
      fabricCanvas.setActiveObject(line).renderAll();
    });
    
    
    
    
    

提交回复
热议问题