You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在FabricJS中绘制自适应长度的带箭头波浪线问题求助

嘿,我帮你搞定这个带箭头的自适应波浪线啦!既然你已经有了直线箭头的基础,咱们直接基于Fabric的自定义对象来扩展,核心思路是动态根据线条长度生成对应数量的波峰,而不是拉伸固定波浪,同时保留你原来的箭头逻辑。

先给你完整的实现步骤和代码:

第一步:创建WavyLineWithArrow自定义类

这个类继承自fabric.Object,专门处理波浪线的生成和箭头绘制,还支持随长度自适应波峰数量:

import { fabric } from 'fabric';

const WavyLineWithArrow = fabric.util.createClass(fabric.Object, {
  type: 'wavy_line_with_arrow',
  initialize(points, options) {
    options || (options = {});
    this.callSuper('initialize', options);
    this.points = points; // 存储起点和终点 [x1, y1, x2, y2]
    this.waveHeight = options.waveHeight || 5; // 波浪的峰谷高度,可自定义
    this.baseWaveCount = 4; // 参考长度对应的波峰数(就是你说的短线条4个波峰)
    this.baseLength = options.baseLength || 100; // 对应4个波峰的线条基准长度,可调整
    // 基础样式设置,和你的直线箭头保持一致
    this.set({
      hasBorders: false,
      hasControls: false,
      strokeWidth: options.strokeWidth || 2,
      stroke: options.stroke || '#000',
      customProps: options.customProps,
    });
  },

  // 重写set方法,支持更新终点坐标
  set({ x2, y2, ...rest }) {
    if (x2 !== undefined && y2 !== undefined) {
      this.points[2] = x2;
      this.points[3] = y2;
    }
    this.callSuper('set', rest);
    this.setCoords();
  },

  // 快捷获取端点坐标
  get x1() { return this.points[0]; },
  get y1() { return this.points[1]; },
  get x2() { return this.points[2]; },
  get y2() { return this.points[3]; },

  // 计算线条的实际长度
  getLineLength() {
    const dx = this.x2 - this.x1;
    const dy = this.y2 - this.y1;
    return Math.sqrt(dx * dx + dy * dy);
  },

  // 生成波浪线的路径字符串(用二次贝塞尔曲线实现平滑波浪)
  generateWavePath() {
    const length = this.getLineLength();
    if (length === 0) return '';

    // 核心:按长度比例计算波峰数量,保证长度翻倍时波峰数也翻倍
    const waveCount = Math.round((length / this.baseLength) * this.baseWaveCount);
    const actualWaveCount = Math.max(waveCount, 1); // 避免长度过短时没有波浪
    const step = length / actualWaveCount; // 每个波峰之间的间隔长度

    // 构建平滑波浪路径:用二次贝塞尔曲线连接每个峰和谷
    let path = `M 0 0`;
    for (let i = 1; i <= actualWaveCount; i++) {
      // 波峰的位置:x轴中间点,y轴为waveHeight
      const peakX = step * (i - 0.5);
      path += ` Q ${peakX} ${this.waveHeight} ${step * i} 0`;
    }
    return path;
  },

  // 渲染波浪线和箭头
  _render(ctx) {
    this.callSuper('_render', ctx);
    const length = this.getLineLength();
    if (length === 0) return;

    // 计算线条的角度,方便后续旋转坐标系
    const dx = this.x2 - this.x1;
    const dy = this.y2 - this.y1;
    const angle = Math.atan2(dy, dx);

    ctx.save();
    // 平移到线条中点,旋转到线条方向,这样画波浪可以沿X轴直接画,不用考虑角度
    ctx.translate(this.x1 + dx / 2, this.y1 + dy / 2);
    ctx.rotate(angle);

    // 设置线条样式
    ctx.strokeStyle = this.stroke;
    ctx.lineWidth = this.strokeWidth;
    ctx.lineCap = 'round';

    // 绘制波浪线
    const wavePath = this.generateWavePath();
    const path = new fabric.Path(wavePath);
    path.render(ctx);

    // 绘制箭头,和你原来的直线箭头逻辑一致
    ctx.beginPath();
    ctx.moveTo(length - 5, 0); // 从终点往前5px开始,避免和线条末端重叠
    ctx.lineTo(length - 15, 5);
    ctx.lineTo(length - 15, -5);
    ctx.closePath();
    ctx.fillStyle = this.stroke;
    ctx.fill();

    ctx.restore();
  },

  // 序列化时保存自定义属性
  toObject() {
    return fabric.util.object.extend(this.callSuper('toObject'), {
      points: this.points,
      waveHeight: this.waveHeight,
      baseWaveCount: this.baseWaveCount,
      baseLength: this.baseLength,
      customProps: this.customProps,
    });
  },

  // 支持从序列化对象加载
  static fromObject(object, callback) {
    const wavyLine = new WavyLineWithArrow(object.points, object);
    callback && callback(wavyLine);
    return wavyLine;
  },
});

// 注册类,方便Fabric序列化识别
fabric.WavyLineWithArrow = WavyLineWithArrow;
fabric.WavyLineWithArrow.fromObject = WavyLineWithArrow.fromObject;

export default WavyLineWithArrow;

第二步:在主代码中添加波浪线绘制方法

和你原来的drawLineWithArrow对应,添加一个drawWavyLineWithArrow:

import WavyLineWithArrow from './WavyLineWithArrow';

// ... 你的其他代码 ...

drawWavyLineWithArrow = (item, points, color) => (
  new WavyLineWithArrow(points, {
    customProps: item,
    strokeWidth: 2,
    stroke: color || colors.BLACK,
    baseLength: 100, // 这里可以调整:当线条长度为100px时,显示4个波峰
    waveHeight: 5, // 波浪高度,按需调整
  })
)

// 然后在selectLine方法中添加case:
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, colors.BLACK); // 新增这个case
    // no default
  }
  return null;
}

关键逻辑说明

  1. 自适应波峰数量:通过baseLength和baseWaveCount控制,比如baseLength=100对应4个波峰,当线条长度变成200时,波峰数自动变成8个,完全符合你的需求,不是拉伸固定波浪,而是动态生成更多波峰。
  2. 平滑波浪:用二次贝塞尔曲线(Q指令)绘制波浪,比折线更自然。
  3. 坐标系旋转:渲染时先把坐标系旋转到线条方向,这样画波浪只需要沿X轴绘制,不用计算每个点的角度,简化了代码。
  4. 兼容原有交互:重写了set方法,支持通过x2、y2更新终点,和你原来的鼠标拖动逻辑完全兼容,不需要修改鼠标事件的代码。

这样你就可以像画直线箭头一样,拖动鼠标绘制带箭头的自适应波浪线啦!

内容的提问来源于stack exchange,提问作者GuerillaRadio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:02:26