繁体   English   中英

Map 一个角度到 2 个其他角度的范围

[英]Map an angle to a range of 2 other angles

我希望这会有意义

在此处输入图像描述

我正在尝试编写一个 function 给定一个角度,计算一个 0.0 到 1.0 的范围,该范围表示圆中的弧角,其中起点(0.0 映射到圆上)和结束(1.0 映射到圆上)是任意的。

start 和 end 可以是从 -π 到 4π 的任何值,但它们的差值总是 <= 2π。

上面的行很重要假设您要 map 整个圆圈。 start = -π, end = +π 有效,但我们希望支持任何映射因此例如从 6 点顺时针回到 6 点,我们不能指定 start = +π/2, end = +π/2但是我们可以用 start = +π/2, end = +π * 2.5 来指定它。

在此处输入图像描述

同样我们可以用另一种方式 go,开始 = +π * 2.5,结束 = +π * 0.5

在此处输入图像描述

请注意映射的方向已更改。 关键是我正在尝试处理任何映射,您需要指定“围绕圆圈”的方法,因此 N 的开始和 N + 2π = 顺时针从 N 围绕整个圆圈的结束,其中作为 N 的开始和结束N - 2π = 从 N 逆时针绕整个圆。

我绊倒的部分是 -π/+π 和从我的角度转换为我的 0.0 到 1.0 值之间的不连续性。

我们称 0.0 开始,1.0 结束。 在上面的第一张图中,开始在 -0.6π 左右,结束在 -0.2π 左右。 在这种情况下,这很容易

result = (angle - startAngle) / (endAngle - startAngle)

如果我的弧很大但想设置 0.0 和 1.0 的位置,它会失败。 例如,如果我将 start 设置为 0.5π(向下)并将 end 设置为 2.5π,那么整个圆圈中 0.0 向下(6 点钟),0.25 向右(9 点钟),0.5 向上(12 点钟) 'clock),0.7 是正确的(3 点钟),1.0 是向后的(6 点钟)

此外,对于红色范围之外的角度,我希望它的一半钳制到 0,另一半钳制到 1.0。 如果我刚刚

result = clamp(results, 0, 1)

将圆圈视为时钟,从 9:00 到 11:00(小区域)我会得到 0,而从 2:00 到 9:00(大)我会得到 1。 我想要的是从 7:00 到 11:00 的 0 和从 2:00 到 7:00 的 1(区域大小相等)

我觉得我必须编写很多特殊情况,并且不断遇到错误,所以我想我会问是否有一种简单的方法可以做到这一点。 换句话说,写 function

result = mapAngleToArc(angle, startAngleInRadians, endAngleInRadians);

我真的不想展示代码,因为我不想让人们走上一条坏路。 我希望有一些涉及点积的简单数学可以计算出来,而不必检查 endAngleInRadians 是否小于 startAngleInRadians .... 或者可能没有。

有些人声称只是在这里或那里添加 2π 就可以解决所有问题。 好吧,我在哪里添加它? . 如果只是给出模糊的提示,也许可以尝试直接回答问题。

这是另一个例子:

在此处输入图像描述

我在哪里添加 2π? 输入

angle = -π <-> +2π
start = π*0.6
end = π*2.4

预期成绩

-角度- -结果-
0.55π 0.0
0.6π 0.0
-0.5π 0.5
0.24π 1.0
0.245π 1.0

我在哪里添加或修改 2π 以获得这些结果?

注意:我真的不在乎哪种语言,但如果我必须选择一种语言,我会选择 JavaScript,因为这只是因为每个人都可以在片段中做一个活生生的例子,但我怀疑有更多 C++ 程序员有这类问题的经验. 无论如何,这是一个可以玩的活生生的例子

 const ctx = document.querySelector('canvas').getContext('2d'); const logElem = document.querySelector('pre'); function drawCircle(ctx, cx, cy, start, end) { ctx.beginPath(); ctx.arc(cx, cy, 70, 0, Math.PI * 2); ctx.fillStyle = 'cyan'; ctx.fill(); ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, 70, start, end); ctx.fillStyle = 'red'; ctx.fill(); const range = end - start; const unRange = Math.PI * 2 - range; // draw 0.0 area ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, 60, start - unRange / 2, start); ctx.fillStyle = 'pink'; ctx.fill(); // draw 1.0 area ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, 60, end, end + unRange / 2); ctx.fillStyle = 'orange'; ctx.fill(); } function drawPoint(ctx, x, y) { ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI * 2); ctx.fillStyle = 'black'; ctx.fill(); } function drawAngle(ctx, px, py, cx, cy) { ctx.save(); ctx.translate(cx, cy); const dx = s.pointX - circleX; const dy = s.pointY - circleY; const angle = Math.atan2(dy, dx); ctx.rotate(angle); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(65, 0); ctx.strokeStyle = 'yellow' ctx.stroke(); ctx.restore(); } const circleX = 150; const circleY = 75; const s = { start: Math.PI * -0.7, end: Math.PI * -0.1, pointX: 160, pointY: 40, }; function radToDeg(rad) { return (rad * 180 / Math.PI).toFixed(1); } function clamp(v, min, max) { return Math.min(max, Math.max(min, v)); } function draw() { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); drawCircle(ctx, circleX, circleY, s.start, s.end); drawAngle(ctx, s.pointX, s.pointY, circleX, circleY); drawPoint(ctx, s.pointX, s.pointY); const dx = s.pointX - circleX; const dy = s.pointY - circleY; const angle = Math.atan2(dy, dx); const result = mapAngleToArc(angle, s.start, s.end); logElem.textContent = `\ angle: ${angle.toFixed(3)} (${radToDeg(angle)}) result: ${result.toFixed(3)} `; } function mapAngleToArc(angle, startAngle, endAngle) { const result = (angle - startAngle) / (endAngle - startAngle); return clamp(result, 0, 1); } const GUI = lil.GUI; const gui = new GUI().onChange(draw); gui.add(s, 'start', -Math.PI, Math.PI * 4); gui.add(s, 'end', -Math.PI, Math.PI * 4); draw(); function handleMove(e) { s.pointX = e.offsetX; s.pointY = e.offsetY; draw(); } function handleUp() { window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleUp); } ctx.canvas.addEventListener('mousedown', e => { window.addEventListener('mousemove', handleMove); window.addEventListener('mouseup', handleUp); handleMove(e); });
 <canvas></canvas> <pre></pre> <p> When in the pink area should return 0.0<br> When in the orange area should return 1.0<br> Along to red area should return 0.0 &lt;-&gt; 1.0<br> Should work for any values of start and end as long a<br> <code>Math.abs(end - start) <= Math.PI * 2</code> </p> <script src="https://cdn.jsdelivr.net/npm/lil-gui@0.17"></script>

// C++ version

float clamp(float v, float min, float max) {
  return std::min(max, std::max(min, v));
}

float mapAngleToArg(float angle, float start, float end) {
  ASSERT(start > -M_PI && start < M_PI * 4);
  ASSERT(end > -M_PI && end < M_PI * 4);
  ASSERT(std::abs(end - start) <= M_PI * 2);

  float result = (angle - start) / (end - start);
  return clamp(result, 0.0f, 1.0f);
}

看起来你在思考你的问题是什么时糊涂了,结果你问了一个非常复杂的问题,而且没有简单的答案。

因此,让我们从您认为容易的事情开始。 正如您所说, “在 start=0.5π, end=2.5π 的情况下没有歧义。很明显,我想要一个围绕圆的范围,从 0.5π 点开始,到 2.5π 点结束。” 我同意这一点。

但是现在您还想处理棘手的情况,例如 start=0.9π, end=-0.9π。 你想让这意味着从 0.9π 到 1.1π 的顺时针弧。 但它也可以合理地表示从 0.9π 到 -0.9π 的逆时针弧。 坚持它应该是顺时针的意味着你的代码有很多 DWIM(按我的意思做)的魔力。 正如您所发现的那样,这意味着堆积边缘情况,因为您的意思并不是到目前为止的代码猜测您的意思。 一旦你明白了你认为它应该是什么意思,你将不可避免地发现很难向别人解释。 当你这样做时,你会发现他们想要它意味着别的东西

但是,如果您允许逆时针解释,那么所有特殊情况都会消失。 您的问题归结为通过添加 2π 的正确倍数来调整 start 在正确的范围内。 然后做同样的事情到最后。 现在方向是结束开始的标志,角度的大小也是如此。

现在没有魔法,没有边缘情况,也没有并发症。 它易于编写,易于解释,易于理解。

(随机说明,您已经颠倒了关于角度的正常约定。通常正弧度表示逆时针移动。我在解释中按照您的图表进行了说明。但如果您希望其他人使用它,我建议切换到标准。)

好吧,这就是我最终得到的

基本上计算开始和结束之间的中心,然后旋转所有内容,开始,结束,角度,通过从所有内容中减去中心来使中心为 0。 这意味着开始和结束现在位于中心的相对侧,并且彼此的负数有效,并且实际上距离中心最远的任何东西都是相对侧。

function clamp(v, min, max) {
  return Math.min(max, Math.max(min, v));
}

function euclideanModulo(v, n) {
  return ((v % n) + n) % n;
}

function twoPiMod(v) {
  return euclideanModulo(v + Math.PI, Math.PI * 2) - Math.PI;
}

function mapAngleToArc(angle, start, end) {
  const center = (start + end) / 2;

  const centeredAngle = twoPiMod(angle - center);
  const centeredStart = twoPiMod(start - center);
  const newV = (centeredAngle - centeredStart) / (end - start);
  return clamp(newV, 0, 1);
}

 const ctx = document.querySelector('canvas').getContext('2d'); const logElem = document.querySelector('pre'); function drawCircle(ctx, cx, cy, start, end) { ctx.beginPath(); ctx.arc(cx, cy, 70, 0, Math.PI * 2); ctx.fillStyle = 'cyan'; ctx.fill(); ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, 70, start, end); ctx.fillStyle = 'red'; ctx.fill(); const range = end - start; const unRange = Math.PI * 2 - range; // draw 0.0 area ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, 60, start - unRange / 2, start); ctx.fillStyle = 'pink'; ctx.fill(); // draw 1.0 area ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, 60, end, end + unRange / 2); ctx.fillStyle = 'orange'; ctx.fill(); } function drawPoint(ctx, x, y) { ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI * 2); ctx.fillStyle = 'black'; ctx.fill(); } function drawAngle(ctx, px, py, cx, cy) { ctx.save(); ctx.translate(cx, cy); const dx = s.pointX - circleX; const dy = s.pointY - circleY; const angle = Math.atan2(dy, dx); ctx.rotate(angle); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(65, 0); ctx.strokeStyle = 'yellow' ctx.stroke(); ctx.restore(); } const circleX = 150; const circleY = 75; const s = { start: Math.PI * -0.7, end: Math.PI * -0.1, pointX: 160, pointY: 40, }; function radToDeg(rad) { return (rad * 180 / Math.PI).toFixed(1); } function clamp(v, min, max) { return Math.min(max, Math.max(min, v)); } function draw() { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); drawCircle(ctx, circleX, circleY, s.start, s.end); drawAngle(ctx, s.pointX, s.pointY, circleX, circleY); drawPoint(ctx, s.pointX, s.pointY); const dx = s.pointX - circleX; const dy = s.pointY - circleY; const angle = Math.atan2(dy, dx); const result = mapAngleToArc(angle, s.start, s.end); const bad = Math.abs(s.start - s.end) > Math.PI * 2; logElem.textContent = bad? 'ERROR: Math.abs(start - end) > Math.PI * 2': `\ angle: ${angle.toFixed(3)} (${radToDeg(angle)}) result: ${result.toFixed(3)} `; } function euclideanModulo(v, n) { return ((v % n) + n) % n; } function twoPiMod(v) { return euclideanModulo(v + Math.PI, Math.PI * 2) - Math.PI; } function mapAngleToArc(angle, start, end) { const center = (start + end) / 2; const centeredAngle = twoPiMod(angle - center); const centeredStart = twoPiMod(start - center); const newV = (centeredAngle - centeredStart) / (end - start); return clamp(newV, 0, 1); } const GUI = lil.GUI; const gui = new GUI().onChange(draw); gui.add(s, 'start', -Math.PI, Math.PI * 4); gui.add(s, 'end', -Math.PI, Math.PI * 4); draw(); function handleMove(e) { s.pointX = e.offsetX; s.pointY = e.offsetY; draw(); } function handleUp() { window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleUp); } ctx.canvas.addEventListener('mousedown', e => { window.addEventListener('mousemove', handleMove); window.addEventListener('mouseup', handleUp); handleMove(e); });
 <canvas></canvas> <pre></pre> <p> When in the pink area should return 0.0<br> When in the orange area should return 1.0<br> Along to red area should return 0.0 &lt;-&gt; 1.0<br> Should work for any values of start and end as long a<br> <code>Math.abs(end - start) <= Math.PI * 2</code> </p> <script src="https://cdn.jsdelivr.net/npm/lil-gui@0.17"></script>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM