给定正多边形的一条边,找出剩余的边
Given one side of a regular polygon, find the remaining sides
我们得到:
- 正多边形的边数
- 一侧由坐标 (x1, y1) 和 (x2, y2) 定义
我必须找出剩余的坐标。我该怎么做?
假设我们有一个 turtle,它通过从 x1, y1
移动到 x2, y2
绘制了 n
边正多边形的一段。要绘制剩余的边,我们这样做 n-1
次:
- 转一定角度
- 前进一定距离
我们必须转动的角度是 360/n
度。例如,如果我们正在绘制一个三角形,则海龟必须在每一段之后转动 120 度。如果我们顺时针绘制三角形,我们将从乌龟的当前方向减去 120 度。如果我们逆时针绘制,则增加 120 度。
前进的距离为第一段的长度。我们可以使用 Pythagorean theorem 来计算段的长度。在JavaScript中,我们可以这样实现:
var dx = x2-x1,
dy = y2-y1,
length = Math.sqrt(dx*dx + dy*dy);
乌龟的初始方向与第一条线段的角度相同,我们可以通过dx/length
的反余弦来计算:
var angle = Math.acos(dx/length);
if (dy < 0) {
angle = 2*Math.PI - angle;
}
要利用转角和线段长度,我们必须实现以下海龟操作:
- 设置海龟位置
x, y
- 设置乌龟方向
angle
- 将乌龟按当前方向向前移动
distance
个单位
- 将
delta
添加到乌龟方向
- 查询海龟当前位置
x, y
实现这些操作后,我们可以编写一个迭代 n-1
次的循环。在每次迭代中,我们转动并向前移动规定的量,查询海龟位置,并打印坐标。
要实现海龟操作,我们必须存储海龟的位置和方向。这是一个简单的方法:
var turtle = { x: 0, y: 0, angle: 0 };
要使乌龟在其当前方向上向前移动 distance
个单位,我们使用基本三角函数:
turtle.x += Math.cos(turtle.angle) * distance;
turtle.y += Math.sin(turtle.angle) * distance;
请注意,JavaScript 中的三角函数使用弧度而不是度数。一个圆有 2π 弧度,因此半圆有 π 弧度。如果我们有一个以弧度表示的角度 r
,则等效的度数是 r / Math.PI * 180
.
当我们从海龟的方向上加上或减去一个值时,可能会得到一个小于零或大于 2π 的角度。这不会影响我们的三角函数计算,但会使程序难以调试。为了确保角度始终在 [0, 2π) 范围内,我们可以在 turtle.angle
被修改时执行以下操作:
turtle.angle -= Math.floor(turtle.angle / (2*Math.PI)) * 2*Math.PI;
我已经编写了一个代码片段来演示海龟方法。 运行 通过单击下面的蓝色按钮来编写代码,然后单击并拖动以绘制多边形的第一段。您可以通过单击加号和减号来更改边数。
var Polygon = {
color: {
axes: '#ccc',
sides: {
hover: { plain: '#dddfa4', special: '#9d9c64' },
final: { plain: '#b0c598', special: '#4f7337' }
}
}
};
Polygon.turtle = { x: 0, y: 0, angle: 0 };
Polygon.turtle.setPosition = function (x, y) {
var g = Polygon,
turtle = g.turtle,
context = g.context,
origin = g.origin;
turtle.x = x;
turtle.y = y;
context.moveTo(origin.left + turtle.x, origin.top - turtle.y);
};
Polygon.turtle.setAngle = function (angle) {
var g = Polygon,
turtle = g.turtle;
turtle.angle = angle;
};
Polygon.turtle.left = function (delta) {
var g = Polygon,
turtle = g.turtle;
turtle.angle = g.normalizeAngle(turtle.angle + delta);
};
Polygon.turtle.right = function (delta) {
var g = Polygon,
turtle = g.turtle;
turtle.angle = g.normalizeAngle(turtle.angle - delta);
};
Polygon.normalizeAngle = function (angle) {
angle -= Math.floor(angle / (2*Math.PI)) * 2*Math.PI;
return angle;
};
Polygon.turtle.forward = function (distance) {
var g = Polygon,
turtle = g.turtle,
canvas = g.canvas,
context = g.context,
origin = g.origin;
turtle.x += Math.cos(turtle.angle) * distance;
turtle.y += Math.sin(turtle.angle) * distance;
context.lineTo(origin.left + turtle.x, origin.top - turtle.y);
};
Polygon.resizeCanvas = function() {
var g = Polygon,
canvas = g.canvas,
context = g.context,
width = canvas.width = window.innerWidth,
height = canvas.height = window.innerHeight;
g.origin = { left: Math.floor(width/2), top: Math.floor(height/2) };
g.drawAxes();
};
Polygon.drawAxes = function() {
var g = Polygon,
canvas = g.canvas,
context = g.context,
origin = g.origin,
color = g.color;
context.lineWidth = 2;
context.strokeStyle = color.axes;
context.beginPath();
context.moveTo(origin.left, 0);
context.lineTo(origin.left, canvas.height);
context.moveTo(0, origin.top);
context.lineTo(canvas.width, origin.top);
context.stroke();
};
Polygon.drawPolygon = function (situation) {
var g = Polygon,
canvas = g.canvas,
context = g.context,
turtle = g.turtle,
color = g.color,
n = parseInt(document.getElementById('numSides').innerHTML, 10),
turn = 2*Math.PI / n,
x1 = g.x1, y1 = g.y1, x2 = g.x2, y2 = g.y2,
dx = x2-x1,
dy = y2-y1,
length = Math.sqrt(dx*dx + dy*dy);
var angle = Math.acos(dx/length);
if (dy < 0) {
angle = 2*Math.PI - angle;
}
context.clearRect(0, 0, canvas.width, canvas.height);
g.drawAxes();
context.lineWidth = 4;
context.lineCap = 'round';
context.beginPath();
context.strokeStyle = color.sides[situation].plain;
turtle.setPosition(x1, y1);
turtle.setAngle(angle);
for (var i = 0; i < n; ++i) {
turtle.forward(length);
turtle.left(turn);
}
context.closePath();
context.stroke();
context.strokeStyle = color.sides[situation].special;
context.beginPath();
turtle.setPosition(x1, y1);
turtle.forward(length);
context.stroke();
}
Polygon.load = function () {
var g = Polygon,
canvas = g.canvas = document.getElementById('surface'),
context = g.context = canvas.getContext('2d'),
display = { begin: document.getElementById('begin'),
end: document.getElementById('end') },
color = g.color;
g.resizeCanvas();
window.onresize = g.resizeCanvas;
function makeUnselectable(element) {
element.className += ' unselectable';
element.ondragstart = element.onselectstart = function (event) {
event.preventDefault();
};
}
makeUnselectable(canvas);
var numSides = document.getElementById('numSides'),
minus = document.getElementById('minus'),
plus = document.getElementById('plus');
minus.onmousedown = function () {
var current = parseInt(numSides.innerHTML, 10);
if (current == 3) {
return;
}
numSides.innerHTML = current-1;
g.drawPolygon('final');
};
plus.onmousedown = function () {
var current = parseInt(numSides.innerHTML, 10);
if (current == 20) {
return;
}
numSides.innerHTML = current+1;
g.drawPolygon('final');
};
var controls = [display.begin, display.end, numSides, minus, plus,
document.getElementById('options')];
for (var i = 0; i < controls.length; ++i) {
makeUnselectable(controls[i]);
}
var getPosition = function (event) {
event = event || window.event;
var rect = canvas.getBoundingClientRect(),
left = event.clientX - rect.left,
top = event.clientY - rect.top,
origin = g.origin,
x = left - origin.left,
y = origin.top - top;
return { x: x, y: y };
};
canvas.onmousedown = function (event) {
document.body.style.cursor = 'default';
var position = getPosition(event);
g.x1 = g.x2 = position.x;
g.y1 = g.y2 = position.y;
display.begin.innerHTML =
'<span class="label">x1, y1 =</span> '+g.x1+', '+g.y1;
display.end.innerHTML = '';
g.drawPolygon('hover');
for (var i = 0; i < controls.length; ++i) {
controls[i].style.zIndex = -10;
}
canvas.onmousemove = function (event) {
var position = getPosition(event);
g.x2 = position.x;
g.y2 = position.y;
display.end.innerHTML =
'<span class="label">x2, y2 =</span> '+g.x2+', '+g.y2;
g.drawPolygon('hover');
};
};
function noop() {
}
canvas.onmousemove = noop;
canvas.onmouseup = canvas.onmouseout = function (event) {
if (canvas.onmousemove === noop) {
return;
}
canvas.onmousemove = noop;
g.drawPolygon('final');
for (var i = 0; i < controls.length; ++i) {
controls[i].style.zIndex = 0;
}
};
};
window.onload = Polygon.load;
body {
margin: 0;
padding: 0;
overflow: hidden;
}
.unselectable {
-webkit-user-select: none;
-khtml-user-drag: none;
-khtml-user-select: none;
-moz-user-select: none;
-moz-user-select: -moz-none;
-ms-user-select: none;
user-select: none;
}
canvas {
width: 100%;
height: 100%;
}
.display {
color: #444;
position: fixed;
left: 40px;
font-family: sans-serif;
font-size: 20px;
}
.label {
color: #aaa;
}
#begin {
top: 20px;
}
#end {
top: 60px;
}
#options {
position: fixed;
left: 40px;
top: 100px;
font-family: sans-serif;
font-size: 28px;
}
#options div {
display: inline;
}
#options .button {
font-size: 32px;
cursor: pointer;
}
#options .button:hover {
color: #55838e;
}
#options .button, #numSides {
padding: 0 5px;
}
#numSides {
cursor: default;
}
<div class="display" id="begin"></div>
<div class="display" id="end"></div>
<div id="options">
<div class="button" id="minus">−</div><div
id="numSides">6</div><div
class="button" id="plus">+</div>
</div>
<canvas id="surface"></canvas>
如果边数为N,多边形的边由坐标(x1,y1)和(x2,y2)定义,则该边的中点为
(mx, my) = ((x1+x2)/2, (y1+y2)/2)
垂直向量(逆时针顺序左一)为
(px, py) = (-(y2-y1), x2-x1)
单位垂直矢量
(nx, ny) = (px/Len, py/Len), where Len = Sqrt(px*px + py*py)
ApoLen = Len / (2*Tan(Pi/N))
apothem 向量
(ax, ay) = ApoLen * (nx, ny) = (px / (2 * Tan(Pi/N)), py / (2 * Tan(Pi/N)))
多边形中心坐标为
(cx, cy) = (mx, my) + (ax, ay)
圆周半径
R = ApoLen / Cos(Pi/N)
顶点 (x1, x2) 的起始角度为
BaseAngle = ArcTan2(y1 - cy, x1 - cy)
第 i 个顶点(从一开始编号)有角度
Ai = BaseAngle + (i - 1) * 2 * Pi / N
和坐标
(xi, yi) = (cx + R * Cos(Ai), cy + R * Sin(Ai))
我们得到:
- 正多边形的边数
- 一侧由坐标 (x1, y1) 和 (x2, y2) 定义
我必须找出剩余的坐标。我该怎么做?
假设我们有一个 turtle,它通过从 x1, y1
移动到 x2, y2
绘制了 n
边正多边形的一段。要绘制剩余的边,我们这样做 n-1
次:
- 转一定角度
- 前进一定距离
我们必须转动的角度是 360/n
度。例如,如果我们正在绘制一个三角形,则海龟必须在每一段之后转动 120 度。如果我们顺时针绘制三角形,我们将从乌龟的当前方向减去 120 度。如果我们逆时针绘制,则增加 120 度。
前进的距离为第一段的长度。我们可以使用 Pythagorean theorem 来计算段的长度。在JavaScript中,我们可以这样实现:
var dx = x2-x1,
dy = y2-y1,
length = Math.sqrt(dx*dx + dy*dy);
乌龟的初始方向与第一条线段的角度相同,我们可以通过dx/length
的反余弦来计算:
var angle = Math.acos(dx/length);
if (dy < 0) {
angle = 2*Math.PI - angle;
}
要利用转角和线段长度,我们必须实现以下海龟操作:
- 设置海龟位置
x, y
- 设置乌龟方向
angle
- 将乌龟按当前方向向前移动
distance
个单位 - 将
delta
添加到乌龟方向 - 查询海龟当前位置
x, y
实现这些操作后,我们可以编写一个迭代 n-1
次的循环。在每次迭代中,我们转动并向前移动规定的量,查询海龟位置,并打印坐标。
要实现海龟操作,我们必须存储海龟的位置和方向。这是一个简单的方法:
var turtle = { x: 0, y: 0, angle: 0 };
要使乌龟在其当前方向上向前移动 distance
个单位,我们使用基本三角函数:
turtle.x += Math.cos(turtle.angle) * distance;
turtle.y += Math.sin(turtle.angle) * distance;
请注意,JavaScript 中的三角函数使用弧度而不是度数。一个圆有 2π 弧度,因此半圆有 π 弧度。如果我们有一个以弧度表示的角度 r
,则等效的度数是 r / Math.PI * 180
.
当我们从海龟的方向上加上或减去一个值时,可能会得到一个小于零或大于 2π 的角度。这不会影响我们的三角函数计算,但会使程序难以调试。为了确保角度始终在 [0, 2π) 范围内,我们可以在 turtle.angle
被修改时执行以下操作:
turtle.angle -= Math.floor(turtle.angle / (2*Math.PI)) * 2*Math.PI;
我已经编写了一个代码片段来演示海龟方法。 运行 通过单击下面的蓝色按钮来编写代码,然后单击并拖动以绘制多边形的第一段。您可以通过单击加号和减号来更改边数。
var Polygon = {
color: {
axes: '#ccc',
sides: {
hover: { plain: '#dddfa4', special: '#9d9c64' },
final: { plain: '#b0c598', special: '#4f7337' }
}
}
};
Polygon.turtle = { x: 0, y: 0, angle: 0 };
Polygon.turtle.setPosition = function (x, y) {
var g = Polygon,
turtle = g.turtle,
context = g.context,
origin = g.origin;
turtle.x = x;
turtle.y = y;
context.moveTo(origin.left + turtle.x, origin.top - turtle.y);
};
Polygon.turtle.setAngle = function (angle) {
var g = Polygon,
turtle = g.turtle;
turtle.angle = angle;
};
Polygon.turtle.left = function (delta) {
var g = Polygon,
turtle = g.turtle;
turtle.angle = g.normalizeAngle(turtle.angle + delta);
};
Polygon.turtle.right = function (delta) {
var g = Polygon,
turtle = g.turtle;
turtle.angle = g.normalizeAngle(turtle.angle - delta);
};
Polygon.normalizeAngle = function (angle) {
angle -= Math.floor(angle / (2*Math.PI)) * 2*Math.PI;
return angle;
};
Polygon.turtle.forward = function (distance) {
var g = Polygon,
turtle = g.turtle,
canvas = g.canvas,
context = g.context,
origin = g.origin;
turtle.x += Math.cos(turtle.angle) * distance;
turtle.y += Math.sin(turtle.angle) * distance;
context.lineTo(origin.left + turtle.x, origin.top - turtle.y);
};
Polygon.resizeCanvas = function() {
var g = Polygon,
canvas = g.canvas,
context = g.context,
width = canvas.width = window.innerWidth,
height = canvas.height = window.innerHeight;
g.origin = { left: Math.floor(width/2), top: Math.floor(height/2) };
g.drawAxes();
};
Polygon.drawAxes = function() {
var g = Polygon,
canvas = g.canvas,
context = g.context,
origin = g.origin,
color = g.color;
context.lineWidth = 2;
context.strokeStyle = color.axes;
context.beginPath();
context.moveTo(origin.left, 0);
context.lineTo(origin.left, canvas.height);
context.moveTo(0, origin.top);
context.lineTo(canvas.width, origin.top);
context.stroke();
};
Polygon.drawPolygon = function (situation) {
var g = Polygon,
canvas = g.canvas,
context = g.context,
turtle = g.turtle,
color = g.color,
n = parseInt(document.getElementById('numSides').innerHTML, 10),
turn = 2*Math.PI / n,
x1 = g.x1, y1 = g.y1, x2 = g.x2, y2 = g.y2,
dx = x2-x1,
dy = y2-y1,
length = Math.sqrt(dx*dx + dy*dy);
var angle = Math.acos(dx/length);
if (dy < 0) {
angle = 2*Math.PI - angle;
}
context.clearRect(0, 0, canvas.width, canvas.height);
g.drawAxes();
context.lineWidth = 4;
context.lineCap = 'round';
context.beginPath();
context.strokeStyle = color.sides[situation].plain;
turtle.setPosition(x1, y1);
turtle.setAngle(angle);
for (var i = 0; i < n; ++i) {
turtle.forward(length);
turtle.left(turn);
}
context.closePath();
context.stroke();
context.strokeStyle = color.sides[situation].special;
context.beginPath();
turtle.setPosition(x1, y1);
turtle.forward(length);
context.stroke();
}
Polygon.load = function () {
var g = Polygon,
canvas = g.canvas = document.getElementById('surface'),
context = g.context = canvas.getContext('2d'),
display = { begin: document.getElementById('begin'),
end: document.getElementById('end') },
color = g.color;
g.resizeCanvas();
window.onresize = g.resizeCanvas;
function makeUnselectable(element) {
element.className += ' unselectable';
element.ondragstart = element.onselectstart = function (event) {
event.preventDefault();
};
}
makeUnselectable(canvas);
var numSides = document.getElementById('numSides'),
minus = document.getElementById('minus'),
plus = document.getElementById('plus');
minus.onmousedown = function () {
var current = parseInt(numSides.innerHTML, 10);
if (current == 3) {
return;
}
numSides.innerHTML = current-1;
g.drawPolygon('final');
};
plus.onmousedown = function () {
var current = parseInt(numSides.innerHTML, 10);
if (current == 20) {
return;
}
numSides.innerHTML = current+1;
g.drawPolygon('final');
};
var controls = [display.begin, display.end, numSides, minus, plus,
document.getElementById('options')];
for (var i = 0; i < controls.length; ++i) {
makeUnselectable(controls[i]);
}
var getPosition = function (event) {
event = event || window.event;
var rect = canvas.getBoundingClientRect(),
left = event.clientX - rect.left,
top = event.clientY - rect.top,
origin = g.origin,
x = left - origin.left,
y = origin.top - top;
return { x: x, y: y };
};
canvas.onmousedown = function (event) {
document.body.style.cursor = 'default';
var position = getPosition(event);
g.x1 = g.x2 = position.x;
g.y1 = g.y2 = position.y;
display.begin.innerHTML =
'<span class="label">x1, y1 =</span> '+g.x1+', '+g.y1;
display.end.innerHTML = '';
g.drawPolygon('hover');
for (var i = 0; i < controls.length; ++i) {
controls[i].style.zIndex = -10;
}
canvas.onmousemove = function (event) {
var position = getPosition(event);
g.x2 = position.x;
g.y2 = position.y;
display.end.innerHTML =
'<span class="label">x2, y2 =</span> '+g.x2+', '+g.y2;
g.drawPolygon('hover');
};
};
function noop() {
}
canvas.onmousemove = noop;
canvas.onmouseup = canvas.onmouseout = function (event) {
if (canvas.onmousemove === noop) {
return;
}
canvas.onmousemove = noop;
g.drawPolygon('final');
for (var i = 0; i < controls.length; ++i) {
controls[i].style.zIndex = 0;
}
};
};
window.onload = Polygon.load;
body {
margin: 0;
padding: 0;
overflow: hidden;
}
.unselectable {
-webkit-user-select: none;
-khtml-user-drag: none;
-khtml-user-select: none;
-moz-user-select: none;
-moz-user-select: -moz-none;
-ms-user-select: none;
user-select: none;
}
canvas {
width: 100%;
height: 100%;
}
.display {
color: #444;
position: fixed;
left: 40px;
font-family: sans-serif;
font-size: 20px;
}
.label {
color: #aaa;
}
#begin {
top: 20px;
}
#end {
top: 60px;
}
#options {
position: fixed;
left: 40px;
top: 100px;
font-family: sans-serif;
font-size: 28px;
}
#options div {
display: inline;
}
#options .button {
font-size: 32px;
cursor: pointer;
}
#options .button:hover {
color: #55838e;
}
#options .button, #numSides {
padding: 0 5px;
}
#numSides {
cursor: default;
}
<div class="display" id="begin"></div>
<div class="display" id="end"></div>
<div id="options">
<div class="button" id="minus">−</div><div
id="numSides">6</div><div
class="button" id="plus">+</div>
</div>
<canvas id="surface"></canvas>
如果边数为N,多边形的边由坐标(x1,y1)和(x2,y2)定义,则该边的中点为
(mx, my) = ((x1+x2)/2, (y1+y2)/2)
垂直向量(逆时针顺序左一)为
(px, py) = (-(y2-y1), x2-x1)
单位垂直矢量
(nx, ny) = (px/Len, py/Len), where Len = Sqrt(px*px + py*py)
ApoLen = Len / (2*Tan(Pi/N))
apothem 向量
(ax, ay) = ApoLen * (nx, ny) = (px / (2 * Tan(Pi/N)), py / (2 * Tan(Pi/N)))
多边形中心坐标为
(cx, cy) = (mx, my) + (ax, ay)
圆周半径
R = ApoLen / Cos(Pi/N)
顶点 (x1, x2) 的起始角度为
BaseAngle = ArcTan2(y1 - cy, x1 - cy)
第 i 个顶点(从一开始编号)有角度
Ai = BaseAngle + (i - 1) * 2 * Pi / N
和坐标
(xi, yi) = (cx + R * Cos(Ai), cy + R * Sin(Ai))