javascript - 在 FabricJS 中绘制一条波浪线

标签 javascript canvas fabricjs

我正在使用 FabricJS创建用于绘制特定线条和形状的 Canvas 。其中一条线是带箭头的波浪线,类似这样:

enter image description here

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

这是我用来绘制带有箭头端点的直线的代码。请注意,线的起点是在 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;

最佳答案

结果

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

结果是:

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

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

免责声明

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

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

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

花哨的箭头类型

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

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>

关于javascript - 在 FabricJS 中绘制一条波浪线,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48223350/

相关文章:

javascript - `fabric.js` 中的神奇数字来自哪里?

javascript - CckEditor将源代码<strike>标签编辑为<s>标签

javascript - Nodejs - 将制表符分隔的文件编写为 json 对象

javascript - 打字时转换为大写,有一些延迟

javascript - 带有弧形 Canvas 的倒数计时器

Javascript Canvas 仅将图像绘制到缓冲区

canvas - 使用 raphael.js 直接进行 Canvas 平移

canvas - 将对象限制在 Canvas 边界时出现问题

javascript - Fabric.js - 需要为多边形使用填充图案和填充颜色

javascript - 未捕获的类型错误 : number is not a function