【问题标题】:Drawing a wavy line in FabricJS在 FabricJS 中绘制波浪线
【发布时间】:2018-06-21 17:51:33
【问题描述】:

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

我已经成功地创建了一个带有箭头端点的直线版本,但找不到任何关于如何创建波浪线的示例。用户可以根据需要绘制线条,因此线条中的“峰”和“谷”的数量将需要相应地调整(如上图所示的短线可能有 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;

【问题讨论】:

  • 你想一直画在中间位置的那个箭头?
  • @Durga 理想情况下,是的
  • 如果线的长度小于波浪在顶部(峰,谷)结束,我的意思是它不会在中间结束,所以你想要什么?
  • 如果是这样的话,我很高兴有一个箭头并不总是在中间

标签: javascript canvas fabricjs


【解决方案1】:

结果

我不是真正的专家,但我尝试自己实现波浪线。

结果如下:

编码

我使用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));

所有艰巨的任务是计算函数值、缩放等。 但这只是无聊的几何图形。

免责声明

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

我看到的唯一一个问题是在您的示例中,您从一个角落到另一个角落渲染了线条。

旋转波浪线没什么大不了的,但这就是我注意到的理想解决方案的所有差异。

花式箭头

我做了以下漂亮的箭头:

// 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上查看那个sn-p

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>

【讨论】:

  • @GuerillaRadio 谢谢 :)
【解决方案2】:

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

演示

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;
  }
  var clone = fabric.util.object.clone;
  fabric.LineWithArrow = fabric.util.createClass(fabric.Line, {
    type: 'lineWithArrow',

    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,
      });
    },
  });
  fabric.LineWithArrow.fromObject = function(object, callback) {
    function _callback(instance) {
      delete instance.points;
      callback && callback(instance);
    };
    var options = clone(object, true);
    options.points = [object.x1, object.y1, object.x2, object.y2];
    fabric.Object._fromObject('LineWithArrow', options, _callback, 'points');
  };
})(typeof exports !== 'undefined' ? exports : this);
canvas {
  border: 2px dotted black;
}
<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>

【讨论】:

  • 这很棒,并且与我当前用于绘制直线的代码无缝结合。非常感谢!
  • @GuerillaRadio 编码快乐 ;)
  • 我同意这是更好的答案,因为它只需要直接画布渲染而不是分组。好东西! :)
猜你喜欢
  • 2016-05-22
  • 1970-01-01
  • 1970-01-01
  • 2014-03-29
  • 2012-07-31
  • 1970-01-01
  • 2016-08-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多