Загрузка данных


<!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>