Dibujando una línea ondulada en FabricJS

Estoy usando FabricJS para crear un canvas para dibujar líneas y forms específicas. Una de las líneas es una línea ondulada con una flecha similar a esta:

enter image description here

He creado con éxito una versión recta de esto con un punto final de flecha, pero no puedo encontrar ningún ejemplo de cómo crear una línea ondulada. El usuario puede dibujar la línea todo el time que quiera, por lo que el número de "picos" y "depresiones" en la línea deberá adaptarse en consecuencia (una línea corta como la de arriba podría tener 4 picos pero una línea dos veces mayor tendría 8 picos, no solo ser una versión estirada de la línea más corta).

Aquí está el código que estoy usando para dibujar la línea recta con el punto final de la flecha. Tenga en count que el punto de inicio de la línea se dibuja en el mousedown y el punto final se dibuja en 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(); }); 

Y el file 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.restre(); }, toObject() { return fabric.util.object.extend(this.callSuper('toObject'), { customProps: this.customProps, }); }, }); export default LineWithArrow; 

Como estamos dibujando desde ambas esquinas para una línea, puede dibujar una línea ondulada en el método _render de la class personalizada. Desde el final trazo una línea a la mitad, para mostrar su connection con la flecha.

MANIFESTACIÓN

 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((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.restre(); 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); 
 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> 

Resultados

No soy realmente un experto, pero intenté implementar líneas onduladas por mi count.

Ese es el resultado:

Captura de pantalla de flechas de codepen.io

Codificación

fabric.Group la fabric.Group class para agrupar líneas que forman nuestra línea ondulada.

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

Las líneas se eliminan y se agregan al object después de cada cambio:

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

La flecha al final de una línea también es un 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)); 

Toda la tarea difícil fue el cálculo de los valores de las funciones, el escalado, etc., pero es una geometry aburrida.

Renuncia

Probé la implementación de mi línea ondulada y parece funcionar bien incluso si admite otra function (que no es un seno).

Solo veo un problema en su ejemplo: representa líneas de esquina a esquina.

No es gran cosa rotar la línea ondulada, pero esas son todas las diferencias con respecto a la solución ideal que noté.

Fantásticos types de flechas

Hice las siguientes bonitas flechas:

Captura de pantalla de tipos de flecha

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

Ejemplo completo

A continuación adjunto mi recorte con líneas onduladas de trabajo.
También puedes ver ese fragment en 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.restre(); }, 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>