我正在使用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;
答案 0 :(得分:4)
我不是真正的专家,但我试图独自实施波浪线。
结果就是这样:
我使用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>
答案 1 :(得分:4)
当我们从一个角的两个角绘制时,您可以在自定义类的_render
方法中绘制波浪线。从最后我画一条线到中间,以显示它与箭头相连。
<强>样本强>
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(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,
});
},
});
})(typeof exports !== 'undefined' ? exports : this);
&#13;
canvas {
border: 2px dotted black;
}
&#13;
<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>
&#13;