I\'m using FabricJS to create a canvas for drawing specific lines and shapes. One of the lines is a wavy line with an arrow similar to this:
I\'ve successfu
I'm not really an expert, but I attempted to implement wavy lines all by myself.
That's the result:
I used the fabric.Group class to group lines that make up our wavy line.
const WavyLineWithArrow = fabric.util.createClass(fabric.Group, {
/* ... */
};
The lines are removed and added to the object after each change:
this.forEachObject(function(o) {
this.remove(o);
}, this);
for(var i=1;i
Arrow at the end of a line is also an 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));
All the hard task was the calculation of function values, scalling etc. but it's just boring geometry.
I tested my wavy line implementation and it seems to work nicely even if you support other function (that is not a sine).
Only one problem I see that's in your example you rendered lines from corner to corner.
It's not a big deal to rotate the wavy line, but that's all the differences from the ideal solution that I noticed.
I made the following nice arrows:
// 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
]
Below I attach my snipped with working wavy lines.
You can also view that snippet on 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 (
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();
});