簡體   English   中英

在 FabricJS 中繪制波浪線

[英]Drawing a wavy line in FabricJS

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

結果

我不是真正的專家,但我試圖自己實現波浪線。

結果是這樣的:

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

所有艱巨的任務都是計算函數值、縮放等,但這只是無聊的幾何。

免責聲明

我測試了我的波浪線實現,即使您支持其他功能(不是正弦),它似乎也能很好地工作。

我看到的只有一個問題,那就是在您的示例中,您從一個角到另一個角渲染了線條。

旋轉波浪線沒什么大不了的,但這就是我注意到的理想解決方案的所有差異。

花式類型的箭頭

我做了以下漂亮的箭頭:

箭頭類型截圖

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

當我們從兩個角繪制一條線時,您可以在自定義類的_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>

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM