gpt4 book ai didi

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

转载 作者:数据小太阳 更新时间:2023-10-29 05:59:59 26 4
gpt4 key购买 nike

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

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com