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


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