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

如何使用正向运动学在Fabric.js/JavaScript中实现带父子联动的2D骨架?

用正向运动学重写Fabric.js火柴人,实现父子节点联动旋转

核心思路

正向运动学(FK)的核心是建立骨骼节点的父子层级依赖:父节点的旋转、位移会传递给所有子节点,子节点基于父节点的坐标系计算自身的最终位置和角度。在Fabric.js中,我们可以通过自定义骨骼对象、递归更新子节点状态来实现这个效果。

实现步骤

  • 自定义骨骼节点:继承Fabric.Rect(或Fabric.Line),扩展parent(父节点)、children(子节点列表)、length(骨骼长度)、localAngle(相对于父节点的局部角度)属性,方便层级管理和FK计算。
  • 构建火柴人骨骼层级:比如设置「躯干」为根节点,「大臂」「大腿」作为躯干的子节点,「小臂」「小腿」分别作为对应大臂、大腿的子节点,形成树状结构。
  • 实现FK更新函数:写一个递归函数,当父节点的位置或角度变化时,计算子节点的起始坐标(父节点的末端位置),再根据父节点的世界角度+子节点局部角度,计算子节点的末端位置,最后递归更新子节点的所有后代。
  • 绑定拖拽交互:给每个骨骼节点添加鼠标拖拽事件,拖拽结束时触发FK更新,同步所有子节点的状态。

代码示例

// 自定义骨骼类
class Bone extends fabric.Rect {
  constructor(options) {
    super(options);
    this.parent = options.parent || null;
    this.children = [];
    this.length = options.length || 50;
    this.localAngle = options.localAngle || 0;
    // 标记骨骼的末端点(用于子节点定位)
    this.endPoint = new fabric.Point(0, 0);
  }

  // 更新自身世界角度和末端点
  updateWorldState() {
    if (this.parent) {
      // 父节点的世界角度 + 自身局部角度 = 自身世界角度
      this.angle = this.parent.angle + this.localAngle;
      // 子节点起始点 = 父节点的末端点
      const startX = this.parent.endPoint.x;
      const startY = this.parent.endPoint.y;
      // 计算自身末端点:起始点 + 长度 * 角度偏移
      const rad = fabric.util.degreesToRadians(this.angle);
      this.left = startX;
      this.top = startY;
      this.endPoint.x = startX + this.length * Math.cos(rad);
      this.endPoint.y = startY + this.length * Math.sin(rad);
    } else {
      // 根节点直接用自身位置计算末端点
      const rad = fabric.util.degreesToRadians(this.angle);
      this.endPoint.x = this.left + this.length * Math.cos(rad);
      this.endPoint.y = this.top + this.length * Math.sin(rad);
    }
  }
}

// 递归更新所有子节点
function updateFK(bone) {
  bone.updateWorldState();
  bone.children.forEach(child => {
    updateFK(child);
  });
}

// 初始化画布和火柴人
const canvas = new fabric.Canvas('canvas');

// 创建根节点:躯干
const torso = new Bone({
  left: 200,
  top: 100,
  width: 10,
  height: 80,
  fill: '#333',
  length: 80,
  angle: 0
});

// 创建大臂(躯干的子节点)
const upperArm = new Bone({
  parent: torso,
  width: 8,
  height: 60,
  fill: '#333',
  length: 60,
  localAngle: -45 // 相对于躯干向左上方45度
});
torso.children.push(upperArm);

// 创建小臂(大臂的子节点)
const lowerArm = new Bone({
  parent: upperArm,
  width: 6,
  height: 50,
  fill: '#333',
  length: 50,
  localAngle: 30 // 相对于大臂向右下方30度
});
upperArm.children.push(lowerArm);

// 添加到画布
canvas.add(torso, upperArm, lowerArm);

// 绑定拖拽事件:拖拽任意节点后更新FK
canvas.on('object:modified', function(e) {
  const target = e.target;
  if (target instanceof Bone) {
    // 如果是子节点,更新其局部角度(相对于父节点)
    if (target.parent) {
      target.localAngle = target.angle - target.parent.angle;
    }
    // 从根节点开始递归更新所有节点
    updateFK(torso);
    canvas.renderAll();
  }
});

关键细节

  • 局部角度 vs 世界角度:子节点的localAngle是相对于父节点的角度,angle是世界角度(父节点角度+局部角度),这样父节点旋转时,子节点会自动继承旋转偏移。
  • 末端点计算:每个骨骼的末端点是子节点的起始点,必须基于父节点的世界坐标和角度计算,确保层级联动准确。
  • 递归更新:必须从根节点开始递归更新,否则会出现子节点状态不同步的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 22:42:32