在FabricJS中画出一条波浪线

时间:2018-01-12 09:38:19

标签: javascript canvas fabricjs

我正在使用FabricJS来创建用于绘制特定线条和形状的画布。其中一条线是一条波浪线,箭头与此类似:

enter image description here

我已经使用箭头端点成功创建了这个版本,但找不到任何如何创建波浪线的示例。用户可以根据需要绘制线条,因此线条中的“峰值”和“波谷”数量需要相应调整(如上图所示的短线可能有4个峰值但是长度为两倍的线条会有8个峰值,不仅仅是较短线的拉伸版本。)

这是我用来绘制带箭头端点的直线的代码。请注意,该行的起点是在mousedown上绘制的,端点是在mouseup上绘制的。

import LineWithArrow from './LineWithArrow';

drawLineWithArrow = (item, points, color) => (
  new LineWithArrow(points, {
    customProps: item,
    strokeWidth: 2,
    stroke: color,
  })
)

selectLine = (item, points) => {
  switch (item.type) {
    case 'line_with_arrow':
      return this.drawLineWithArrow(item, points, colors.BLACK);

    case 'wavy_line_with_arrow':
      return this.drawWavyLineWithArrow(item, points);
    // no default
  }
  return null;
}

let line;
let isDown;

fabricCanvas.on('mouse:down', (options) => {
  isDown = true;
  const pointer = fabricCanvas.getPointer(options.e);
  const points = [pointer.x, pointer.y, pointer.x, pointer.y];
  line = this.selectLine(item, points);
  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();
});

LineWithArrow文件:

import { fabric } from 'fabric';

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,
    });
  },
});

export default LineWithArrow;

2 个答案:

答案 0 :(得分:4)

结果

我不是真正的专家,但我试图独自实施波浪线。

结果就是这样:

Screenshot of arrows from codepen.io

编码

我使用fabric.Group类对构成我们波浪线的线进行分组。

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

每次更改后,行都会被删除并添加到对象中:

this.forEachObject(function(o) {
    this.remove(o);
}, this);

for(var i=1;i<polyPoints.length;++i) {
    this.add(new fabric.Line([
      polyPoints[i-1].x,
      polyPoints[i-1].y,
      polyPoints[i].x,
      polyPoints[i].y
    ], options));
  }

一行末尾的箭头也是一个对象:

  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));

所有艰巨的任务是计算功能值,剥落等。 但它只是无聊的几何形状。

声明

我测试了我的波浪线实现,即使你支持其他功能(这不是正弦),它似乎也很好用。

我只看到一个问题,在你的例子中,你是从一个角落到另一个角落。

旋转波浪线并不是什么大不了的事,但这与我注意到的理想解决方案有所不同。

箭头的花式类型

我制作了以下漂亮的箭头:

Arrow types screenshot

// 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
]

完整示例

下面我附上剪了工作的波浪线 您还可以在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<len-headSize-step; x+=step) {
        polyPoints.push({
          x: x-len/2,
          y: this._funct_(x*scaleFactorX)*scaleFactorY
        });
      }
      
      // Push the begin of straing line at the end of arrow
      polyPoints.push({x: len/2-headSize-step, y: 0});
      // Push the end of arrow
      polyPoints.push({x: len/2, y: 0});
      
      // Remove old objects
      this.forEachObject(function(o) {
        this.remove(o);
      }, this);
      
      // Add new one
      for(var i=1;i<polyPoints.length;++i) {
        this.add(new fabric.Line([
          polyPoints[i-1].x,
          polyPoints[i-1].y,
          polyPoints[i].x,
          polyPoints[i].y
        ], options));
      }
      
      // This code creates polyline (little triangle at the arrow end)
      const arrOptions = fabric.util.object.clone(options);
      arrOptions.left = len/2;
      arrOptions.top = -arrowSize/2;
      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));
      
    };
  
    // Call super constructor
    this.callSuper('initialize', [], selfOptions);
    
    // Synchronize data
    this.updateInternalPointsData();
    
    // Set default options
    this.set({
      hasBorders: true,
      hasControls: true,
    });
  },

  render(ctx) {
    this.updateInternalPointsData();
    this.callSuper('render', ctx);
  },

  toObject() {
    return fabric.util.object.extend(this.callSuper('toObject'), {
      customProps: this.customProps,
      x1: this.x1,
      x2: this.x2,
      y1: this.y1,
      y2: this.y2,
      arrowSize: this.arrowSize,
      period: this.period,
      funct: this._funct_
    });
  },
});

drawLineWithArrow = (item, points, color) => (
  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();
});
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.14.2/TweenMax.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.8/fabric.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<canvas id="c"></canvas>

答案 1 :(得分:4)

当我们从一个角的两个角绘制时,您可以在自定义类的_render方法中绘制波浪线。从最后我画一条线到中间,以显示它与箭头相连。

<强>样本

&#13;
&#13;
var line, isDown, evented;
var canvas = new fabric.Canvas('canvas', {
  perPixelTargetFind: true
});
draw();

function selection() {
  changeObjSelection(true);
  canvas.off('mouse:down');
  canvas.off('mouse:move');
  canvas.off('mouse:up');
  evented = false;
}

function draw() {
  changeObjSelection(false);
  if (!evented) {
    canvas.on('mouse:down', onMouseDown);
    canvas.on('mouse:move', onMouseMove);
    canvas.on('mouse:up', onMouseUp);
    evented = true;
  }
}

function clearCanvas() {
 canvas.clear();
}

function changeObjSelection(value) {
  canvas.selection = value;
  canvas.forEachObject(function(obj) {
    obj.selectable = value;
  })
  canvas.requestRenderAll();
}

function onMouseDown(options) {
  isDown = true;
  var pointer = canvas.getPointer(options.e);
  var points = [pointer.x, pointer.y, pointer.x, pointer.y];
  line = selectLine(points);
  canvas.add(line);
}

function onMouseMove(options) {
  if (!isDown) return;
  var pointer = canvas.getPointer(options.e);
  line.set({
    x2: pointer.x,
    y2: pointer.y
  });
  canvas.renderAll();

}

function onMouseUp(options) {
  isDown = false;
  line.setCoords();
  canvas.requestRenderAll();
}

function drawLineWithArrow(points, color) {
  return new fabric.LineWithArrow(points, {
    strokeWidth: 2,
    stroke: color,
    objectCaching: false,
    selectable: false
  })
}

function selectLine(points) {
  return drawLineWithArrow(points, 'black');
}

//Wavy line

(function(global) {
  'use strict';
  if (fabric.LineWithArrow) {
    fabric.warn('fabric.LineWithArrow is already defined.');
    return;
  }
  fabric.LineWithArrow = fabric.util.createClass(fabric.Line, {
    type: 'line_with_arrow',

    initialize: function(element, options) {
      options || (options = {});
      this.callSuper('initialize', element, options);

      // Set default options
      this.set({
        hasBorders: false,
        hasControls: false,
      });
    },

    _render: function(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(xDiff / 2, yDiff / 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();
      var p = this.calcLinePoints();
      var point = this.pointOnLine(this.point(p.x2, p.y2), this.point(p.x1, p.y1), 10)
      this.wavy(this.point(p.x1, p.y1), point, this.point(p.x2, p.y2), ctx);
      ctx.stroke();
    },

    point: function(x, y) {
      return {
        x: x,
        y: y
      };
    },

    wavy: function(from, to, endPoint, ctx) {
      var cx = 0,
        cy = 0,
        fx = from.x,
        fy = from.y,
        tx = to.x,
        ty = to.y,
        i = 0,
        step = 4,
        waveOffsetLength = 0,

        ang = Math.atan2(ty - fy, tx - fx),
        distance = Math.sqrt((fx - tx) * (fx - tx) + (fy - ty) * (fy - ty)),
        amplitude = -10,
        f = Math.PI * distance / 30;

      for (i; i <= distance; i += step) {
        waveOffsetLength = Math.sin((i / distance) * f) * amplitude;
        cx = from.x + Math.cos(ang) * i + Math.cos(ang - Math.PI / 2) * waveOffsetLength;
        cy = from.y + Math.sin(ang) * i + Math.sin(ang - Math.PI / 2) * waveOffsetLength;
        i > 0 ? ctx.lineTo(cx, cy) : ctx.moveTo(cx, cy);
      }
      ctx.lineTo(to.x, to.y);
      ctx.lineTo(endPoint.x, endPoint.y);
    },

    pointOnLine: function(point1, point2, dist) {
      var len = Math.sqrt(((point2.x - point1.x) * (point2.x - point1.x)) + ((point2.y - point1.y) * (point2.y - point1.y)));
      var t = (dist) / len;
      var x3 = ((1 - t) * point1.x) + (t * point2.x),
        y3 = ((1 - t) * point1.y) + (t * point2.y);
      return new fabric.Point(x3, y3);
    },

    toObject: function() {
      return fabric.util.object.extend(this.callSuper('toObject'), {
        customProps: this.customProps,
      });
    },
  });
})(typeof exports !== 'undefined' ? exports : this);
&#13;
canvas {
  border: 2px dotted black;
}
&#13;
<script src="https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<button type="button" onclick="selection()">selection</button>
<button type="button" onclick="draw()">draw</button>
<button type="button" onclick="clearCanvas()">clear</button>
<canvas id="canvas" width="400" height="400"></canvas>
&#13;
&#13;
&#13;