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


<!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,
        .gore button {
            display: none;
        }
    }
</style>
</head>

<body>

<div class="controls">

    <h1>Развёртка глобуса Ø1000 мм</h1>

    <p>
        24 лепестка × 15° долготы.
        Диаметр сферы: 1000 мм.
        Радиус: 500 мм.
        Припусков для склейки нет.
    </p>

    <p>
        Параллели: каждые 5°.
        Цвет сетки: #f8f8f8.
    </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;
const GORE_DEG = 360 / GORES;    // 15°

const DEG = Math.PI / 180;


/* ============================================================
   ГЕОМЕТРИЯ ЛЕПЕСТКА

   Используется классическая геометрия globe gore.

   theta = угловое расстояние от северного полюса.

   x = R * theta

   Половина ширины лепестка:

   y = R * tan(GORE_DEG / 2) * sin(theta)

   Поэтому границы:
       - сходятся точно в полюсах;
       - имеют максимальную ширину на экваторе;
       - имеют правильную синусоидальную форму.

   При 24 лепестках:
       угол одного лепестка = 15°
       половина = 7.5°
   ============================================================ */

const HALF_GORE_RAD = (GORE_DEG / 2) * DEG;

const GORE_HALF_WIDTH =
    R * Math.tan(HALF_GORE_RAD);

const GORE_WIDTH_EQUATOR =
    2 * GORE_HALF_WIDTH;

const GORE_HEIGHT =
    Math.PI * R;


/* ============================================================
   ТЕХНИЧЕСКИЙ ЗАПАС SVG
   ============================================================ */

const MARGIN = 8;

const SVG_WIDTH =
    GORE_WIDTH_EQUATOR + 2 * MARGIN;

const SVG_HEIGHT =
    GORE_HEIGHT + 2 * MARGIN;


/* ============================================================
   ФОРМАТИРОВАНИЕ ЧИСЕЛ
   ============================================================ */

function f(n) {
    return Number(n.toFixed(3));
}


/* ============================================================
   ТОЧКА ЛЕПЕСТКА

   latDeg:
       широта от -90° до +90°

   lonOffsetDeg:
       отклонение от центрального меридиана.

   ВАЖНО:

   Для физического gore координата X определяется
   расстоянием вдоль меридиана:

       x = R * theta

   а поперечная координата:

       y = R * tan(lambda) * sin(theta)

   Для границы lambda = ±7.5°.
   ============================================================ */

function point(latDeg, lonOffsetDeg) {

    const lat = latDeg * DEG;
    const lon = lonOffsetDeg * DEG;

    // Угловое расстояние от северного полюса
    const theta = Math.PI / 2 - lat;

    const x = R * theta;

    const y =
        R *
        Math.tan(lon) *
        Math.sin(theta);

    return {
        x: x + MARGIN,
        y: y + SVG_WIDTH / 2
    };
}


/* ============================================================
   ТОЧКА С НОРМАЛЬНЫМ ПОРЯДКОМ КООРДИНАТ

   SVG:
       X = от северного полюса к южному
       Y = поперёк лепестка
   ============================================================ */

function gorePoint(latDeg, lonOffsetDeg) {

    const lat = latDeg * DEG;
    const lon = lonOffsetDeg * DEG;

    const theta = Math.PI / 2 - lat;

    const x = R * theta + MARGIN;

    const y =
        SVG_WIDTH / 2 +
        R * Math.tan(lon) * Math.sin(theta);

    return {
        x: x,
        y: y
    };
}


/* ============================================================
   ГРАНИЦА ЛЕПЕСТКА
   ============================================================ */

function makeEdge(offsetDeg) {

    const N = 360;

    let d = "";

    for (let i = 0; i <= N; i++) {

        const lat =
            -90 + 180 * i / N;

        const p =
            gorePoint(lat, offsetDeg);

        if (i === 0) {
            d += `M ${f(p.x)} ${f(p.y)}`;
        } else {
            d += ` L ${f(p.x)} ${f(p.y)}`;
        }
    }

    return d;
}


/* ============================================================
   ПАРАЛЛЕЛЬ

   Каждая широта соответствует:

       theta = 90° - latitude

   На этой широте поперечная ширина лепестка:

       y = ± R * tan(7.5°) * sin(theta)

   Линия параллели в развёртке получается прямой.

   Это соответствует классическому gore-шаблону:
   при сборке края соседних лепестков образуют
   соответствующую широту сферы.
   ============================================================ */

function makeParallel(latDeg) {

    const left =
        gorePoint(latDeg, -GORE_DEG / 2);

    const right =
        gorePoint(latDeg, GORE_DEG / 2);

    return `
        M ${f(left.x)} ${f(left.y)}
        L ${f(right.x)} ${f(right.y)}
    `;
}


/* ============================================================
   СОЗДАНИЕ SVG ОДНОГО ЛЕПЕСТКА
   ============================================================ */

function createGoreSVG(index) {

    const leftLon =
        -GORE_DEG / 2;

    const rightLon =
        GORE_DEG / 2;


    /* --------------------------------------------------------
       КОНТУР
       -------------------------------------------------------- */

    const N = 360;

    let contour = "";

    // Левая граница:
    // от северного полюса к южному

    for (let i = 0; i <= N; i++) {

        const lat =
            90 - 180 * i / N;

        const p =
            gorePoint(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 =
            gorePoint(lat, rightLon);

        contour +=
            ` L ${f(p.x)} ${f(p.y)}`;
    }

    contour += " Z";


    /* --------------------------------------------------------
       ПАРАЛЛЕЛИ КАЖДЫЕ 5°
       -------------------------------------------------------- */

    let parallels = "";

    for (let lat = -85; lat <= 85; lat += 5) {

        const d =
            makeParallel(lat);

        parallels += `
            <path
                d="${d}"
                fill="none"
                stroke="#f8f8f8"
                stroke-width="0.35"
                vector-effect="non-scaling-stroke"
            />
        `;
    }


    /* --------------------------------------------------------
       ЭКВАТОР

       Остаётся тем же цветом #f8f8f8,
       но немного заметнее.
       -------------------------------------------------------- */

    const equator =
        makeParallel(0);


    /* --------------------------------------------------------
       МЕРИДИАНЫ ВНУТРИ ЛЕПЕСТКА

       Для наглядности добавляем центральный меридиан.
       Он является линией сгиба/ориентиром.

       0° относительно центра лепестка.
       -------------------------------------------------------- */

    const centerMeridian =
        makeEdge(0);


    /* --------------------------------------------------------
       ИНФОРМАЦИЯ
       -------------------------------------------------------- */

    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"
    />


    <!-- =====================================================
         ПАРАЛЛЕЛИ КАЖДЫЕ 5°
         ===================================================== -->

    <g>
        ${parallels}
    </g>


    <!-- Экватор -->
    <path
        d="${equator}"
        fill="none"
        stroke="#f8f8f8"
        stroke-width="0.45"
        vector-effect="non-scaling-stroke"
    />


    <!-- =====================================================
         ЦЕНТРАЛЬНЫЙ МЕРИДИАН
         ===================================================== -->

    <path
        d="${centerMeridian}"
        fill="none"
        stroke="#eeeeee"
        stroke-width="0.35"
        vector-effect="non-scaling-stroke"
    />


    <!-- =====================================================
         КОНТУР ЛЕПЕСТКА
         ===================================================== -->

    <path
        d="${contour}"
        fill="none"
        stroke="#777777"
        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
            — 15° долготы
            — центральный меридиан
            ${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);
}


/* ============================================================
   СКАЧИВАНИЕ ВСЕХ 24 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>