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

带约束条件的元素旋转实现问题求助

旋转约束的原生JS实现方案

问题背景

已通过PointerEvents实现元素连续旋转无跳转,但需添加以下约束:

  • 当元素上的N指向正北时,仅允许逆时针旋转;
  • 当N未指向正北时,可双向旋转直至N回到正北;
  • 整体仅允许旋转一整圈。

之前的约束代码无法稳定生效,快速拖动会突破限制,现提供原生JS解决方案:

修改后的完整代码

HTML(无改动)

<div id="app">
  <div class="box">
    <div class="direction north">N</div>
    <div class="direction east">E</div>
    <div class="direction south">S</div>
    <div class="direction west">W</div>
  </div>
</div>

CSS(无改动)

body {
  margin: 0;
  height: 100vh;
}

#app {
  display: grid;
  place-items: center;
  height: inherit;
}

.box {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  align-items: center;
  justify-items: center;
  width: 200px;
  height: 200px;
  background-color: #bbd6b8;
  border: 1px solid #94af9f;
  cursor: move;
}

.direction {
  width: 50px;
  height: 50px;
  background-color: #333;
  border-radius: 50%;
  color: white;
  text-align: center;
  line-height: 50px;
  user-select: none;
}

.north {
  grid-column: 2;
  grid-row: 1;
}

.east {
  grid-column: 3;
  grid-row: 2;
}

.south {
  grid-column: 2;
  grid-row: 3;
}

.west {
  grid-column: 1;
  grid-row: 2;
}

JavaScript(核心修改)

const box = document.querySelector('.box');
const { x, y, width } = box.getBoundingClientRect();
let boxCenter = width / 2;
let boxXPos = x;
let boxYPos = y;
let totalRotation = 0; // 跟踪总旋转角度,范围0-360
let offsetDegree = 0;
let currentDegree = 0;

function calcDegree(x, y) {
  const degree = Math.floor(Math.atan2(y, x) * (180 / Math.PI) + 90);
  return degree < 0 ? 360 + degree : degree;
}

function onPointerMove(event) {
  const x = event.clientX - boxXPos - boxCenter;
  const y = event.clientY - boxYPos - boxCenter;
  currentDegree = calcDegree(x, y);

  // 规范化角度变化,解决跨0/360度边界的跳变问题
  let delta = currentDegree - offsetDegree;
  if (delta > 180) delta -= 360;
  if (delta < -180) delta += 360;

  // 计算目标旋转角度
  let targetRotation = totalRotation + delta;
  let clampedRotation;

  // 应用旋转约束
  if (totalRotation === 0) {
    // N指向正北时,仅允许逆时针旋转(角度≥0)
    clampedRotation = Math.max(0, Math.min(360, targetRotation));
  } else if (totalRotation === 360) {
    // 已完成一整圈,禁止继续旋转
    clampedRotation = 360;
  } else {
    // N未指向正北时,双向旋转但不超出0-360范围
    clampedRotation = Math.max(0, Math.min(360, targetRotation));
  }

  box.style.transform = `rotate(${clampedRotation}deg)`;
}

box.addEventListener('pointerdown', (event) => {
  box.setPointerCapture(event.pointerId);
  const x = event.clientX - boxXPos - boxCenter;
  const y = event.clientY - boxYPos - boxCenter;
  offsetDegree = calcDegree(x, y);

  box.addEventListener('pointermove', onPointerMove);
  box.addEventListener(
    'pointerup',
    () => {
      // 更新总旋转角度为约束后的值
      totalRotation = ((box.style.transform.match(/rotate\((\d+)deg\)/) || [0, 0])[1]) * 1;
      box.removeEventListener('pointermove', onPointerMove);
    },
    { once: true }
  );
});

window.addEventListener('resize', () => {
  const { x, y } = box.getBoundingClientRect();
  boxXPos = x;
  boxYPos = y;
});

关键逻辑说明

  1. 角度变化规范化:
    处理delta值时,通过判断是否超过180度来修正角度循环问题,避免快速拖动时角度跳变导致约束失效。

  2. 分状态约束:

    • 正北状态(totalRotation=0):将目标角度限制在0-360之间,禁止顺时针旋转(角度为负的情况会被钳制到0);
    • 中间状态:允许双向旋转,但角度不能超出0-360的范围;
    • 完成一圈(totalRotation=360):固定角度为360,禁止继续旋转。
  3. 状态持久化:
    在pointerup时从元素的transform样式中读取实际应用的角度,更新totalRotation,确保下次操作的初始状态符合约束。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 14:52:29