容器与图片同步旋转:获取真实占位尺寸方案

0 阅读5分钟

1. 核心目标

业务场景

在一个可拖拽、可旋转的图片编辑器中,用户点击"旋转 90°"按钮后:

  • 图片需要原地旋转(中心点不动)
  • 旋转后要能拿到图片实际占据的位置和尺寸lefttopwidthheight
  • 这些数据用于后续的碰撞检测、边界限制、保存布局等

技术痛点

如果直接用 transform: rotate(90deg) 旋转图片:

  • 视觉上:图片确实转了 90° ✅
  • 但 DOM 尺寸offsetWidthoffsetHeight 还是旋转前的值 ❌
  • 原因transform 只改变视觉效果,不改变元素的真实布局尺寸

解决方案

通过内外两层结构实现:

  • 外层容器:通过交换 width/height 改变真实尺寸,让浏览器更新 DOM 尺寸
  • 内层包装器:通过 transform: rotate() 实现视觉旋转
  • 手动纠偏:调整包装器的宽高属性,确保旋转后完美填充容器

最终效果:既能拿到真实的 offsetWidth/Height,又能看到平滑的旋转动画 ✅


2. 解决方案:三步走策略

第一步:外部容器(负责"物理变形")

  • 动作:直接交换容器的 widthheight
  • 目的:让浏览器认为这个盒子真的从"横向"变成了"竖向",从而更新 offsetWidth/offsetHeight

第二步:内部包装器(被动跟随容器)

  • 机制:通过 width: 100%; height: 100% 让包装器自动匹配容器的尺寸。
  • 结果:容器变竖向后,包装器也被动挤成竖向。

第三步:包装器自转 + 手动纠偏(核心逻辑)

问题的本质:旋转导致方向反转

我们用 "老板与员工" 来理解整个流程:

场景 1:旋转 90°

  1. 老板(容器)先变形

    • 老板把自己从 200×100(横向)改成 100×200(竖向)
  2. 员工(包装器)被动跟随

    • 员工因为宽高 100%,被老板挤成 100×200(竖向)✅
  3. 员工自己旋转

    • 员工执行 rotate(90deg)
    • 问题:竖向的 100×200 转 90° 后,变成横向的视觉效果 ❌
    • 和老板要求的竖向完全相反!
  4. 解决方案:先捏成横向,再旋转

    • 既然直接旋转会反向,那我们就先把员工捏成横向200×100
    • 然后对这个横向框执行 rotate(90deg) → 转完正好变成竖向
    if (isVertical) {
      // 员工原本是竖向的,但直接旋转会变成横向
      // 所以要先把员工"捏成"横向,再旋转才能变成竖向
      imgWrap.style.width = state.h + 'px';   // 200px
      imgWrap.style.height = state.w + 'px';  // 100px
    }
    

场景 2:旋转 180°

  1. 老板(容器) 转了一圈又变回 200×100(横向)

  2. 员工(包装器) 被动跟随,也是 200×100(横向)

  3. 员工自己旋转

    • 员工执行 rotate(180deg)
    • 横向转 180° → 还是横向 ✅
    • 天生匹配,无需调整!

3. 完整实现代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>获取旋转后真实宽高 Demo</title>
  <style>
    body { padding: 50px; background: #f0f0f0; }
    .stage { position: relative; width: 500px; height: 500px; background: #ddd; border: 2px solid #999; }
​
    /* 外部容器:通过改变真实尺寸来获取 offsetWidth/Height */
    .box {
      position: absolute;
      transition: all 0.3s ease;
      overflow: hidden; /* 裁剪掉包装器溢出部分 */
      background: white; border: 2px solid blue;
    }
​
    /* 内部包装器:负责居中和内容旋转 */
    .img-wrap {
      position: absolute; left: 50%; top: 50%;
      transform-origin: center center;
      transition: all 0.3s ease;
    }
    .img-wrap img { width: 100%; height: 100%; object-fit: cover; }
​
    button { padding: 8px 16px; margin-right: 10px; cursor: pointer; }
    .log { font-family: monospace; background: #fff; padding: 5px; display: inline-block; }
  </style>
</head>
<body>
<div style="margin-bottom: 20px;">
  <button onclick="rotate(90)">右转 90°</button>
  <button onclick="rotate(-90)">左转 90°</button>
  <button onclick="reset()">重置</button>
  <div class="log" id="log"></div>
</div><div class="stage">
  <div class="box" id="box">
    <div class="img-wrap" id="imgWrap">
      <img src="https://picsum.photos/200/100" alt="示例">
    </div>
  </div>
</div><script>
  // 状态记录:存储盒子的位置、尺寸和旋转角度
  let state = { left: 100, top: 100, w: 200, h: 100, deg: 0 };
  const box = document.getElementById('box');
  const imgWrap = document.getElementById('imgWrap');
  const log = document.getElementById('log');
​
  function render() {
    // 1. 更新外部容器的物理位置和尺寸(这就是我们想要的真实占位)
    box.style.left = state.left + 'px';
    box.style.top = state.top + 'px';
    box.style.width = state.w + 'px';
    box.style.height = state.h + 'px';
​
    // 2. 【关键】让员工(包装器)的宽高属性匹配旋转后的视觉效果
    // 流程拆解:
    //   ① 老板(容器)先从 200×100 变成 100×200(竖向)
    //   ② 员工(包装器)因宽高 100%,被动挤成 100×200(竖向)
    //   ③ 员工执行 rotate(90deg),但竖向框转 90° 会变成横向 → 方向反了!
    //   ④ 解决方案:先把员工"捏成"横向(200×100),再旋转就正好变成竖向
    const isVertical = (Math.abs(state.deg) % 180 === 90);
    if (isVertical) {
      // 老板现在是竖向的,员工要先捏成横向,再旋转才能变成竖向
      imgWrap.style.width = state.h + 'px';
      imgWrap.style.height = state.w + 'px';
    } else {
      // 老板还是横向的,员工保持原样,旋转后方向正确
      imgWrap.style.width = state.w + 'px';
      imgWrap.style.height = state.h + 'px';
    }
​
    // 3. 执行视觉旋转
    imgWrap.style.transform = `translate(-50%, -50%) rotate(${state.deg}deg)`;
​
    // 打印真实占位信息(可用于碰撞检测、边界限制等)
    log.innerHTML = `真实宽: ${box.offsetWidth}px | 真实高: ${box.offsetHeight}px | 角度: ${state.deg}°`;
  }
​
  function rotate(angle) {
    // 记录旋转前的中心点(确保原地旋转)
    const cx = state.left + state.w / 2;
    const cy = state.top + state.h / 2;
​
    // 交换宽高(模拟物理旋转)
    [state.w, state.h] = [state.h, state.w];
​
    // 重新计算左上角,确保中心点不动
    state.left = cx - state.w / 2;
    state.top = cy - state.h / 2;
​
    state.deg += angle;
    render();
  }
​
  function reset() {
    state = { left: 100, top: 100, w: 200, h: 100, deg: 0 };
    render();
  }
​
  render();
</script>
</body>
</html>

4. 总结

核心价值

  • 解决痛点transform 旋转无法获取真实 DOM 尺寸
  • 实现效果:既能拿到准确的 offsetWidth/Height,又有平滑的旋转动画
  • 应用场景:图片编辑器、拖拽布局、碰撞检测、边界限制

核心思想:三步走 + 预补偿

  1. 老板变形:容器交换宽高,改变物理尺寸

  2. 员工跟随:包装器通过宽高 100% 被动匹配容器

  3. 员工自转并调整

    • 如果直接旋转会反向 → 先捏成相反的形状,再旋转就对了
    • 如果旋转后方向正确 → 无需调整

记忆口诀

  • 老板变竖了 → 员工被挤成竖向 → 但竖向转 90° 会变横向 → 所以要先捏成横向,再转才变竖向
  • 老板还是横的 → 员工也是横向 → 横向转 180° 还是横向 → 不用调整

本质原理

rotate(90deg) 会让元素的方向反转。为了让旋转后的结果符合预期,我们需要在旋转前预补偿——把元素捏成相反的形状,这样转完就正好对了!