Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Развёртка глобуса Ø1000 мм — 24 лепестка</title>
<style>
body {
margin: 0;
padding: 20px;
background: #eeeeee;
font-family: Arial, sans-serif;
color: #333;
}
h1 {
font-size: 22px;
margin: 0 0 8px;
}
p {
margin: 5px 0;
font-size: 14px;
}
.controls {
position: sticky;
top: 0;
z-index: 10;
background: white;
padding: 15px;
margin: -20px -20px 20px;
border-bottom: 1px solid #ccc;
}
button {
padding: 8px 14px;
margin: 5px 5px 0 0;
border: 1px solid #999;
background: #fafafa;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #e8e8e8;
}
.gore {
background: white;
margin: 20px auto;
padding: 15px;
width: fit-content;
box-shadow: 0 1px 5px #bbb;
}
.gore h2 {
font-size: 15px;
font-weight: normal;
margin: 0 0 8px;
}
svg {
display: block;
background: white;
}
@media screen {
.gore svg {
max-width: 90vw;
height: auto;
}
}
@media print {
body {
background: white;
padding: 0;
}
.controls {
display: none;
}
.gore {
box-shadow: none;
margin: 0;
padding: 0;
page-break-after: always;
}
.gore h2 {
display: none;
}
}
</style>
</head>
<body>
<div class="controls">
<h1>Развёртка глобуса Ø1000 мм</h1>
<p>
24 лепестка × 15° долготы = 24 часа.
Масштаб: 1:1. Припусков для склейки нет.
</p>
<button onclick="downloadAll()">Скачать все 24 SVG</button>
<button onclick="window.print()">Печать 1:1</button>
</div>
<div id="goreContainer"></div>
<script>
/*
============================================================
ПАРАМЕТРЫ ГЛОБУСА
============================================================
*/
const DIAMETER = 1000; // мм
const R = DIAMETER / 2; // радиус = 500 мм
const GORES = 24; // 24 лепестка
const GORE_DEG = 360 / GORES; // 15° = 1 час
const DEG = Math.PI / 180;
/*
Высота лепестка от Южного до Северного полюса:
πR = 1570.796 мм
На экваторе ширина лепестка:
R × 15° × π/180 = 130.900 мм
Это половина? Нет:
границы находятся на ±7.5°, поэтому полная ширина:
2 × R × 7.5° × π/180 = 130.900 мм
*/
/*
============================================================
ПРОЕКЦИЯ ЛЕПЕСТКА
============================================================
Для широты φ:
x = R × λ × cos(φ)
y = R × (φ + π/2)
где λ — отклонение от центрального меридиана лепестка.
При λ = ±7.5° получаются две кривые границы лепестка.
Такая развёртка точно стыкует все 24 лепестка по меридианам
и после сборки образует сферу диаметром 1000 мм.
*/
const goreWidthEquator =
2 * R * (GORE_DEG / 2) * DEG;
const goreHeight = Math.PI * R;
/*
SVG имеет небольшой технический запас вокруг рисунка,
но сам контур лепестка не имеет никакого припуска.
*/
const MARGIN = 8;
const SVG_WIDTH = goreWidthEquator + 2 * MARGIN;
const SVG_HEIGHT = goreHeight + 2 * MARGIN;
/*
Форматирование чисел для SVG
*/
function f(n) {
return Number(n.toFixed(3));
}
/*
============================================================
СОЗДАНИЕ КООРДИНАТЫ ТОЧКИ ЛЕПЕСТКА
============================================================
*/
function point(latDeg, lonOffsetDeg) {
const lat = latDeg * DEG;
const lon = lonOffsetDeg * DEG;
const x = R * lon * Math.cos(lat);
const y = R * (lat + Math.PI / 2);
return {
x: x + SVG_WIDTH / 2,
y: y + MARGIN
};
}
/*
============================================================
КРИВАЯ ГРАНИЦЫ
============================================================
*/
function makeEdge(offsetDeg) {
const N = 180;
let d = "";
for (let i = 0; i <= N; i++) {
const lat = -90 + 180 * i / N;
const p = point(lat, offsetDeg);
if (i === 0)
d += `M ${f(p.x)} ${f(p.y)}`;
else
d += ` L ${f(p.x)} ${f(p.y)}`;
}
return d;
}
/*
============================================================
ПАРАЛЛЕЛЬ
============================================================
*/
function makeParallel(latDeg) {
const left = point(latDeg, -GORE_DEG / 2);
const right = point(latDeg, GORE_DEG / 2);
/*
Параллель — дуга, а не прямая.
Для достаточно точного отображения разбиваем её
на множество маленьких отрезков.
*/
const N = 80;
let d = "";
for (let i = 0; i <= N; i++) {
const lon =
-GORE_DEG / 2 +
GORE_DEG * i / N;
const p = point(latDeg, lon);
if (i === 0)
d += `M ${f(p.x)} ${f(p.y)}`;
else
d += ` L ${f(p.x)} ${f(p.y)}`;
}
return d;
}
/*
============================================================
СОЗДАНИЕ SVG ОДНОГО ЛЕПЕСТКА
============================================================
*/
function createGoreSVG(index) {
const leftLon = -GORE_DEG / 2;
const rightLon = GORE_DEG / 2;
const leftEdge = makeEdge(leftLon);
const rightEdge = makeEdge(rightLon);
/*
Параллели от -80° до +80°.
Полюса не рисуем как параллель — там они сходятся
в одну точку.
*/
let parallels = "";
for (let lat = -80; lat <= 80; lat += 10) {
const d = makeParallel(lat);
parallels += `
<path
d="${d}"
fill="none"
stroke="#c9c9c9"
stroke-width="0.45"
vector-effect="non-scaling-stroke"
/>
`;
}
/*
Экватор немного заметнее остальных параллелей,
но всё ещё остаётся тонким серым.
*/
const equator = makeParallel(0);
/*
Контур лепестка.
*/
const outline = `
${leftEdge}
${rightEdge}
M ${f(point(90, 0).x)} ${f(point(90, 0).y)}
M ${f(point(-90, 0).x)} ${f(point(-90, 0).y)}
`;
/*
Полный замкнутый контур.
*/
const top = point(90, 0);
const bottom = point(-90, 0);
const outlinePath = `
M ${f(top.x)} ${f(top.y)}
${makeEdge(leftLon).replace(/^M [^ ]+ [^ ]+/, "")}
${makeEdge(rightLon).replace(/^M [^ ]+ [^ ]+/, "")}
Z
`;
/*
На практике для замыкания лучше явно сформировать
контур из левой границы вверх-вниз и правой в обратную сторону.
*/
const N = 180;
let contour = "";
// Левая граница: от Южного полюса к Северному
for (let i = 0; i <= N; i++) {
const lat = -90 + 180 * i / N;
const p = point(lat, leftLon);
if (i === 0)
contour += `M ${f(p.x)} ${f(p.y)}`;
else
contour += ` L ${f(p.x)} ${f(p.y)}`;
}
// Правая граница: от Северного к Южному
for (let i = N; i >= 0; i--) {
const lat = -90 + 180 * i / N;
const p = point(lat, rightLon);
contour += ` L ${f(p.x)} ${f(p.y)}`;
}
contour += " Z";
/*
Номер лепестка и диапазон долготы.
*/
const centerLon = index * GORE_DEG;
const centerLonNormalized =
((centerLon + 180) % 360) - 180;
const hour = index + 1;
/*
SVG.
*/
return `
<svg
id="gore-${hour}"
xmlns="http://www.w3.org/2000/svg"
width="${f(SVG_WIDTH)}mm"
height="${f(SVG_HEIGHT)}mm"
viewBox="0 0 ${f(SVG_WIDTH)} ${f(SVG_HEIGHT)}"
>
<!-- Белый фон -->
<rect
x="0"
y="0"
width="${f(SVG_WIDTH)}"
height="${f(SVG_HEIGHT)}"
fill="white"
/>
<!-- Параллели через 10° -->
<g>
${parallels}
</g>
<!-- Экватор -->
<path
d="${equator}"
fill="none"
stroke="#b8b8b8"
stroke-width="0.6"
vector-effect="non-scaling-stroke"
/>
<!-- Контур лепестка -->
<path
d="${contour}"
fill="none"
stroke="#888888"
stroke-width="0.65"
vector-effect="non-scaling-stroke"
/>
</svg>
`;
}
/*
============================================================
ОТОБРАЖЕНИЕ ВСЕХ 24 ЛЕПЕСТКОВ
============================================================
*/
const container = document.getElementById("goreContainer");
const svgStrings = [];
for (let i = 0; i < GORES; i++) {
const svg = createGoreSVG(i);
svgStrings.push(svg);
const wrapper = document.createElement("div");
wrapper.className = "gore";
const centerLon =
((i * GORE_DEG + GORE_DEG / 2 + 180) % 360) - 180;
wrapper.innerHTML = `
<h2>
Лепесток ${i + 1} из 24
— 1 час,
центральный меридиан ${centerLon.toFixed(1)}°
</h2>
${svg}
<button onclick="downloadGore(${i})">
Скачать SVG
</button>
`;
container.appendChild(wrapper);
}
/*
============================================================
СКАЧИВАНИЕ ОДНОГО SVG
============================================================
*/
function downloadGore(index) {
const blob = new Blob(
[svgStrings[index]],
{ type: "image/svg+xml;charset=utf-8" }
);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download =
`globe_1000mm_gore_${String(index + 1).padStart(2, "0")}.svg`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
/*
============================================================
СКАЧИВАНИЕ ВСЕХ SVG
============================================================
*/
function downloadAll() {
svgStrings.forEach((svg, index) => {
setTimeout(() => {
const blob = new Blob(
[svg],
{ type: "image/svg+xml;charset=utf-8" }
);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download =
`globe_1000mm_gore_${String(index + 1).padStart(2, "0")}.svg`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}, index * 150);
});
}
</script>
</body>
</html>