为什么在scale后的父元素上设置子元素的offset会出现异常,如何解决?

作者站长头像
站长
· 阅读数 21

我尝试在一个经过transform:scale()修改后的父元素“playground”上设置其子元素“ball”的offset属性,但是过程中出现了难以描述的异常,似乎每一次设置offset,其都会与offset所提供的数值有所偏差,必须经过多次修改,其才能正确地符合offset提供的位置。

我的代码如下:

    var new_scale = parseInt(一个输入值,小于3且大于0.3)
    $("#playground").css({
        transform: 'scale(' + new_scale + ')',
    });
$("#ball").on("click",function(){
    $("#ball").offset({
            left: 300,
            top: 300
    })
})

我猜想,或许可能与offset不兼容scale,或者scale并没有直接修改元素的位置有关,但是我的代码非常简单,我就只是希望ball能够到达这么一个位置,事实上他的确能移动,但是需要多次点击才行,我毫无头绪,希望能够得到任何建议,谢谢你们!

回复
1个回答
avatar
test
2024-06-26

首先,安装一个 Matrix 库,因为自己写的话,一是麻烦易错,二是性能不佳:

npm install gl-mat3

封装一个仿射矩阵类

const mat3 = require("gl-mat3");

class AffineMatrix {
  #values = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);

  /**
   * @param {[number, number, number, number, number, number]} values
   */
  constructor(values) {
    for (const i in values) {
      this.#values[i] = values[i];
    }
  }

  /**
   * @methd getValues 读取矩阵的值
   * @returns {Float32Array}
   */
  getValues() {
    return new Float32Array(this.#values);
  }

  /**
   * @method rotate 旋转
   * @param {number|string} [rad] 旋转的角度,如果是数字,默认单位是弧度(rad)
   * @returns {AffineMatrix} 返回一个全新矩阵,即不在原矩阵上修改
   */
  //   rotate(rad = 0) {
  //     let radius = rad;
  //     if (typeof rad === "string") {
  //       if (/rad$/.test(rad)) {
  //         radius = parseFloat(rad.replace(/rad$/, ""));
  //       } else if (/deg/.test(rad)) {
  //         radius = parseFloat(rad.replace(/deg$/, "") / Math.PI);
  //       } else {
  //         radius = parseFloat(rad);
  //       }
  //     }
  //     return new AffineMatrix(
  //       mat3.rotate(new Float32Array(9), this.#values),
  //       radius
  //     );
  //   }

  /**
   * @method scale - 缩放
   * @param {number} scaleX
   * @param {number} scaleY
   * @returns {AffineMatrix}
   *
   * @overload
   * @param {*} scaleX
   * @returns {AffineMatrix}
   */
  scale(scaleX, scaleY) {
    if (scaleY === undefined) {
      return new AffineMatrix(
        mat3.scale(new Float32Array(9), this.#values, [scaleX, scaleX, 1])
      );
    } else {
      return new AffineMatrix(
        mat3.scale(new Float32Array(9), this.#values, [scaleX, scaleY, 1])
      );
    }
  }

  /**
   * @method multi 乘以一个向量或另外的矩阵
   * @param {AffineMatrix} obj
   * @returns {AffineMatrix}
   *
   * @overload
   * @param {ArrayLike} obj
   * @returns {Float32Array}
   */
     multi(obj) {
       if (obj instanceof AffineMatrix) {
         return new AffineMatrix(
           mat3.multiply(new Float32Array(9), this.#values, obj.getValues())
         );
       } else {
         const v = this.#values;
         const [x, y, z] = obj;
         return new Float32Array([
           v[0] * x + v[1] * y + v[2] * z,
           v[3] * x + v[4] * y + v[5] * z,
           v[6] * x + v[7] * y + v[8] * z,
         ]);
       }
     }

  /**
   * @method invert 求逆矩阵
   */
  invert() {
    return new AffineMatrix(mat3.invert(new Float32Array(9), this.#values));
  }

  toCSSMatrix() {
    const v = this.#values;
    return `matrix(${v[0]},${v[1]},${v[2]},${v[3]},${v[4]},${v[5]})`;
  }

  /**
   * @method fromCSSMatrix toCSSMatrix 的逆运算
   * @param {string} matrixString
   */
  static fromCSSMatrix(matrixString) {
    return new AffineMatrix([
      ...matrixString
        .replace(/^matrix\(/, "")
        .split(",")
        .map((i) => parseFloat(i)),
      0,
      0,
      1,
    ]);
  }
}

jQuery 上新增一个对应的方法:

$.fn.transform = function(affine){
  this.style.transform = affine.toCSSMatrix();
  return $(this);
}

给父元素设置transform的时候,一律不准使用 $.fn.css,转而使用上述方法:

const fnCss = $.fn.css;
const $background = $("#background");
$.fn.css = function(...args){
  const [val] = args;
  if($background.eq(this) && (val === "transfrom" || "transform" in val)){
    throw new Error("禁止对 #playground 使用 .css('transfrom') ,应使用 transform()")
  }
  return $.fn.css.call(this, ...args);
}

// 在所有变换开始前,获取父元素位置和变换中心位置坐标,后面回来计算很麻烦
const {top, left, width, height} = $background[0].getBoundingClientRect();
const pOrgin = [top + width / 2, left + height / 2];

$background
  .transform(
    (new AffineMatrix([1, 0, 0, 0, 1, 0, 0, 0, 1]))
      .scale(new_scale)
  )

子元素设置 offset 的时候究竟遇到什么问题,题目描述得不是很清楚,因此这里假设是期望子元素的位置免受scale影响。

$("#ball").click(function(){
  const $this = $(this);
  const offsetX = 300;
  const offsetY = 300;

  // 计算目标 offset 在变换前的坐标
  const originOffset = [offsetX + left - pOrgin[0], offsetY + top - pOrgin[1], 1];

  // 计算目标 offset 在变换后的坐标
  const scaledOffset =
    AffineMatrix
      .fromCSSMatrix($background[0].style.transform)
      .multi(originOffset);

  // 计算该坐标点变换前后的移动路径(向量)
  const offsetVector = [
    scaledOffset[0] - originOffset[0],
    scaledOffset[1] - originOffset[1]
  ]

  // 沿着向量反向移动,就可以“回到”变化前应有的位置
  $this.offset({
    offsetX - offsetVector[0],
    offsetY - offsetVector[1]
  })
});

写到这里发现想复杂了,这个方法可以扩展到任意 transform 2D 变换,实际上如果只考虑 scale 的话所有的计算可以进一步简化,计算难度降低到小学高年级数学水平。因为真正的“干货”其实只在于计算那个offsetVector,它的计算公式不过是把矩阵运算展开为线性方程组,很简单:

$$ vx = (x + left - (left + width \div 2)) \times scale - (x + left - (left + width \div 2))\\ vy = (y + top - (top + height \div 2)) \times scale - (x + top - (top + height \div 2)) $$

然后让变换后的点“移动”回去就行了:

$$ x' = (x + left - (left + width \div 2)) \times scale - vx \\ y' = (y + top - (top + height \div 2)) \times scale - vy $$

回复
likes
适合作为回答的
  • 经过验证的有效解决办法
  • 自己的经验指引,对解决问题有帮助
  • 遵循 Markdown 语法排版,代码语义正确
不该作为回答的
  • 询问内容细节或回复楼层
  • 与题目无关的内容
  • “赞”“顶”“同问”“看手册”“解决了没”等毫无意义的内容