Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Глобус Ø1000 мм — генератор лепестка</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 24px;
background: #eeeeee;
font-family: Arial, sans-serif;
color: #222;
}
.panel {
max-width: 1100px;
margin: 0 auto 20px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 3px 15px rgba(0,0,0,.12);
}
h1 {
margin: 0 0 16px;
font-size: 22px;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 14px;
align-items: end;
}
label {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 13px;
}
input, select, button {
height: 38px;
padding: 0 12px;
border: 1px solid #bbb;
border-radius: 6px;
background: white;
font-size: 14px;
}
button {
cursor: pointer;
border: none;
background: #222;
color: white;
font-weight: bold;
}
button:hover {
background: #444;
}
.info {
margin-top: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
}
#preview {
max-width: 1100px;
margin: auto;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 3px 15px rgba(0,0,0,.12);
overflow: auto;
}
#svgPreview {
display: block;
width: 100%;
height: auto;
max-height: 850px;
background: white;
}
</style>
</head>
<body>
<div class="panel">
<h1>Развёртка глобуса Ø 1000 мм — 24 лепестка</h1>
<div class="controls">
<label>
Диаметр глобуса, мм
<input id="diameter" type="number" value="1000" min="100">
</label>
<label>
Количество лепестков
<select id="gores">
<option value="24" selected>24</option>
</select>
</label>
<label>
Лепесток №
<select id="goreNumber"></select>
</label>
<button onclick="generate()">Обновить SVG</button>
<button onclick="downloadSVG()">Скачать SVG</button>
</div>
<div class="info">
<b>Параметры:</b><br>
Лепесток имеет ширину 15° по долготе.
Центральный меридиан проходит через середину лепестка.
Параллели и промежуточные меридианы нанесены через 5°.
Линии сетки: #F8F8F8, 0.3 мм.
Контур и центральный меридиан: серый, 0.3 мм.
Клапанов для склейки нет.
</div>
</div>
<div id="preview">
<svg id="svgPreview"
xmlns="http://www.w3.org/2000/svg">
</svg>
</div>
<script>
const NS = "http://www.w3.org/2000/svg";
const BORDER_COLOR = "#999999";
const GRID_COLOR = "#f8f8f8";
const STROKE_MM = 0.3;
let currentSVG = null;
/*
Геометрия
R = радиус глобуса.
Один из 24 лепестков занимает:
360 / 24 = 15 градусов
Центральный меридиан:
longitude = 0°
Границы:
longitude = -7.5°
longitude = +7.5°
Для остальных лепестков центральный меридиан
просто сдвигается на 15°.
*/
function degToRad(deg) {
return deg * Math.PI / 180;
}
/*
Ширина лепестка на данной широте.
Используется геометрия "orange peel":
x = R * cos(latitude) * sin(longitude)
Это дает нулевую ширину на полюсах
и максимальную ширину на экваторе.
*/
function xAt(latDeg, lonDeg, R) {
const lat = degToRad(latDeg);
const lon = degToRad(lonDeg);
return R * Math.cos(lat) * Math.sin(lon);
}
/*
Y — длина меридианной дуги.
От экватора:
y = R * latitude
Поэтому от экватора до полюса:
R * PI/2
*/
function yAt(latDeg, R) {
return R * degToRad(latDeg);
}
/*
Формирование SVG path
*/
function createPath(points) {
if (!points.length) return "";
let d = "M " + points[0][0] + " " + points[0][1];
for (let i = 1; i < points.length; i++) {
d += " L " + points[i][0] + " " + points[i][1];
}
return d;
}
/*
Создание лепестка
*/
function buildGore(diameter, goreNumber) {
const R = diameter / 2;
const goreWidth = 360 / 24;
/*
Центральная долгота данного лепестка.
*/
const centerLon =
(goreNumber - 1) * goreWidth;
const leftLon =
centerLon - goreWidth / 2;
const rightLon =
centerLon + goreWidth / 2;
/*
Высота от северного до южного полюса.
*/
const height = Math.PI * R;
/*
Максимальная ширина.
*/
const maxHalfWidth =
R * Math.sin(degToRad(goreWidth / 2));
const width =
maxHalfWidth * 2;
/*
Добавляем запас только в SVG viewBox,
но сам контур остается без припуска.
*/
const padding = 5;
const svgWidth = width + padding * 2;
const svgHeight = height + padding * 2;
/*
Перевод математических координат
в SVG координаты.
*/
function sx(x) {
return x + svgWidth / 2;
}
function sy(y) {
/*
Северный полюс сверху.
*/
return y + padding;
}
/*
SVG
*/
const svg =
document.createElementNS(NS, "svg");
svg.setAttribute("xmlns", NS);
svg.setAttribute(
"width",
svgWidth + "mm"
);
svg.setAttribute(
"height",
svgHeight + "mm"
);
svg.setAttribute(
"viewBox",
`0 0 ${svgWidth} ${svgHeight}`
);
/*
Белый фон.
*/
const background =
document.createElementNS(NS, "rect");
background.setAttribute("x", 0);
background.setAttribute("y", 0);
background.setAttribute(
"width",
svgWidth
);
background.setAttribute(
"height",
svgHeight
);
background.setAttribute(
"fill",
"white"
);
svg.appendChild(background);
/*
Группа сетки.
*/
const grid =
document.createElementNS(NS, "g");
grid.setAttribute(
"fill",
"none"
);
grid.setAttribute(
"stroke",
GRID_COLOR
);
grid.setAttribute(
"stroke-width",
STROKE_MM
);
grid.setAttribute(
"vector-effect",
"non-scaling-stroke"
);
/*
ПАРАЛЛЕЛИ
каждые 5 градусов.
*/
for (let lat = -85; lat <= 85; lat += 5) {
const points = [];
/*
Для параллели строим кривую
от левой границы до правой.
*/
for (
let lon = leftLon;
lon <= rightLon;
lon += 0.1
) {
/*
Для выбранного лепестка
longitude переводится относительно
его центрального меридиана.
*/
const localLon =
lon - centerLon;
const x =
xAt(lat, localLon, R);
const y =
yAt(-lat + 90, R);
points.push([
sx(x),
sy(y)
]);
}
const path =
document.createElementNS(NS, "path");
path.setAttribute(
"d",
createPath(points)
);
grid.appendChild(path);
}
/*
МЕРИДИАНЫ
каждые 5 градусов.
В пределах одного лепестка:
-7.5°
-5°
0°
+5°
+7.5°
*/
const meridians = [
leftLon,
centerLon - 5,
centerLon,
centerLon + 5,
rightLon
];
for (const lon of meridians) {
/*
Не рисуем центральный меридиан здесь,
чтобы позже сделать его серым.
*/
if (Math.abs(lon - centerLon) < 0.00001) {
continue;
}
const points = [];
for (
let lat = 90;
lat >= -90;
lat -= 0.25
) {
const localLon =
lon - centerLon;
const x =
xAt(lat, localLon, R);
const y =
yAt(90 - lat, R);
points.push([
sx(x),
sy(y)
]);
}
const path =
document.createElementNS(NS, "path");
path.setAttribute(
"d",
createPath(points)
);
grid.appendChild(path);
}
svg.appendChild(grid);
/*
КОНТУР ЛЕПЕСТКА
*/
const leftPoints = [];
const rightPoints = [];
for (
let lat = 90;
lat >= -90;
lat -= 0.25
) {
const x =
xAt(lat, leftLon - centerLon, R);
const y =
yAt(90 - lat, R);
leftPoints.push([
sx(x),
sy(y)
]);
}
for (
let lat = -90;
lat <= 90;
lat += 0.25
) {
const x =
xAt(lat, rightLon - centerLon, R);
const y =
yAt(90 - lat, R);
rightPoints.push([
sx(x),
sy(y)
]);
}
const outlinePoints =
leftPoints.concat(rightPoints);
/*
Замыкаем контур.
*/
outlinePoints.push(leftPoints[0]);
const outline =
document.createElementNS(NS, "path");
outline.setAttribute(
"d",
createPath(outlinePoints)
);
outline.setAttribute(
"fill",
"none"
);
outline.setAttribute(
"stroke",
BORDER_COLOR
);
outline.setAttribute(
"stroke-width",
STROKE_MM
);
outline.setAttribute(
"vector-effect",
"non-scaling-stroke"
);
svg.appendChild(outline);
/*
ЦЕНТРАЛЬНЫЙ МЕРИДИАН
серый 0.3 мм.
*/
const centerPoints = [];
for (
let lat = 90;
lat >= -90;
lat -= 0.25
) {
const x = 0;
const y =
yAt(90 - lat, R);
centerPoints.push([
sx(x),
sy(y)
]);
}
const centerPath =
document.createElementNS(NS, "path");
centerPath.setAttribute(
"d",
createPath(centerPoints)
);
centerPath.setAttribute(
"fill",
"none"
);
centerPath.setAttribute(
"stroke",
BORDER_COLOR
);
centerPath.setAttribute(
"stroke-width",
STROKE_MM
);
centerPath.setAttribute(
"vector-effect",
"non-scaling-stroke"
);
svg.appendChild(centerPath);
/*
Экватор — параллель 0°.
Делаем его также немного заметнее,
поскольку это важная линия сборки.
*/
const equatorPoints = [];
for (
let lon = leftLon;
lon <= rightLon;
lon += 0.1
) {
const localLon =
lon - centerLon;
const x =
xAt(0, localLon, R);
const y =
yAt(90, R);
equatorPoints.push([
sx(x),
sy(y)
]);
}
const equator =
document.createElementNS(NS, "path");
equator.setAttribute(
"d",
createPath(equatorPoints)
);
equator.setAttribute(
"fill",
"none"
);
equator.setAttribute(
"stroke",
BORDER_COLOR
);
equator.setAttribute(
"stroke-width",
STROKE_MM
);
equator.setAttribute(
"vector-effect",
"non-scaling-stroke"
);
svg.appendChild(equator);
/*
Маленькие подписи для контроля.
Они находятся ВНЕ printable-контура.
В итоговый SVG не добавляются.
*/
return svg;
}
/*
Основная генерация
*/
function generate() {
const diameter =
parseFloat(
document.getElementById("diameter").value
);
const goreNumber =
parseInt(
document.getElementById("goreNumber").value
);
const svg =
buildGore(
diameter,
goreNumber
);
const preview =
document.getElementById("svgPreview");
preview.replaceWith(svg);
svg.id = "svgPreview";
currentSVG = svg;
}
/*
Заполняем список 1–24
*/
function fillGoreList() {
const select =
document.getElementById("goreNumber");
select.innerHTML = "";
for (let i = 1; i <= 24; i++) {
const option =
document.createElement("option");
option.value = i;
option.textContent =
`Лепесток ${i} (${((i - 1) * 15 - 180).toFixed(1)}° … ${((i) * 15 - 180).toFixed(1)}°)`;
select.appendChild(option);
}
/*
По умолчанию — лепесток с центральным
меридианом 0°.
*/
select.value = 13;
}
/*
Скачивание SVG
*/
function downloadSVG() {
if (!currentSVG) {
generate();
}
/*
Клонируем SVG,
чтобы ничего лишнего не менять
в окне предпросмотра.
*/
const clone =
currentSVG.cloneNode(true);
clone.setAttribute(
"xmlns",
"http://www.w3.org/2000/svg"
);
const serializer =
new XMLSerializer();
const source =
serializer.serializeToString(clone);
const blob =
new Blob(
[
'<?xml version="1.0" encoding="UTF-8"?>\n',
source
],
{
type: "image/svg+xml;charset=utf-8"
}
);
const url =
URL.createObjectURL(blob);
const diameter =
document.getElementById("diameter").value;
const gore =
document.getElementById("goreNumber").value;
const a =
document.createElement("a");
a.href = url;
a.download =
`globe_${diameter}mm_gore_${gore}.svg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/*
Запуск
*/
fillGoreList();
generate();
</script>
</body>
</html>