* Не привлекая внимания санитаров

Меня зовут Илья, я занимаюсь фронтенд‑разработкой вот уже 10 лет. Представьте, что вам нужно сделать стили для печати документов, а бегать к принтеру с линейкой, чтобы убедиться в корректности фактических размеров отдельных элементов, очень не хочется. Было бы куда проще иметь возможность приложить ту же линейку к экрану. Но размеры элементов на экране почти всегда не соответствуют их физическим размерам при печати. Казалось бы, зачем это вообще может быть кому‑то нужно. Но это бывает важно. Например, в типографиях.

Как вы уже догадались, работаю я не в типографии, поэтому с подобной проблемой не сталкивался. Но в свободное время интересуюсь необычными задачами из мира веб‑разработки, так сказать, расширяю кругозор. И вот однажды я услышал тезис, что добиться точного воспроизведения реальных размеров на экране невозможно.

Что ж, вызов был брошен. И я поставил себе задачу (вы же тоже сами ставите себе задачи?) — нарисовать красивый красный квадратик размером 3 × 3 см. Тому, что у меня в итоге получилось, и посвящён мой необычный рассказ.

Подготовка

План простой. Верстаю красивый красный квадратик размером 3 × 3 см. Собираю по квартире ПЭВМ разных классов и размеров. Привожу их в порядок. Беру метр и начинаю измерения.

Решение в лоб

Код квадрата достаточно прост:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title></title>
    <link rel="icon" href="data:image/png;base64,">
    <style>
        *,
        :before,
        :after {
            box-sizing: border-box;
        }

        html, body {
            width: 100vw;
            height: 100vh;
            margin: 0;
        }

        body {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .square {
            width: 3cm;
            height: 3cm;
            background: red;
        }
    </style>
</head>
<body>
    <div class="square"></div>
</body>
</html>

Можете сами взять в руки линейку и замерить, что у вас получилось.

Красивый красный квадратик свёрстан. Отцентрирован. Размещён на сервере и измерен почти на всём, что я нашёл в закромах. Хорошая новость — красивый красный квадратик отображается на всех шайтан‑машинах, плохая — он ни разу не 3 × 3 см.

Результаты

Таблица устройств и их характеристик с результатами измерений:

Ну и фото, чтобы оценить масштабы катастрофы. Разброд и шатание. Задача не решена.

Дело в том, что 1 дюйм = 2,54 см = 96 пикселей. Всё. Это не зависит от устройства. Одним словом, в CSS нет абсолютных единиц измерения, а только относительные (не берём стили для печати). Не буду растекаться мыслью по древу, вот статья, где об этом написано подробнее, и спецификация.

Решение на основе диагонали экрана

Вы знаете размеры экрана устройства, с которого вы читаете данное безобразие? Ширину и высоту — почти уверен, что нет, а диагональ — скорее всего, да (или можете легко узнать). Для массовых железяк диагональ указана в характеристиках. Я так её и нашёл для своего зоопарка.

Допустим, диагональ мы знаем: нашли, заняли, украли, узнали от окружения (приложение для телевизора или смартфона), получили с сервера, она прилетела из космоса или её ввёл пользователь.

Диагональ — это гипотенуза прямоугольного треугольника. Соотношение сторон берём из разрешения из window.screen (window.screen.width и window.screen.height). Зная гипотенузу и соотношение катетов из формулы теоремы Пифагора x^{2}+y^{2}=d^{2}, после нехитрых преобразований получаем такую формулу:

x = sqrt(\frac{d^{2}}{1 + k^{2}})y = k × x

где x — ширина, y — высота, d — диагональ, k — соотношение ширины и высоты.

Далее берём ширину и делим на window.screen.width. И получаем количество пикселей на физический дюйм. Я буду работать с сантиметрами.

Его величество код
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title></title>
    <link rel="icon" href="data:image/png;base64,">
    <style>
        *,
        :before,
        :after {
            box-sizing: border-box;
        }

        :root {
            --pixes-per-cm: 0;
        }

        html, body {
            width: 100vw;
            height: 100vh;
            margin: 0;
        }

        body {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .square {
            width: clamp(3px, calc(3 * var(--pixes-per-cm)), 100%);
            height: clamp(3px, calc(3 * var(--pixes-per-cm)), 100%);
            background: red;
        }

        input {
            width: 3cm;
            margin-top: 20px;
        }

        .panel {
            position: fixed;
            top: 0;
            left: 0;
        }
    </style>
</head>
<body>
    <div class="panel">
        <p>Разрешение: <b class="resolution"></b></p>
        <p>Физические размеры: <b class="sizes"></b></p>
        <p>Пикселей/см: <b class="pxs-per-cm"></b></p>
    </div>
    <div class="square"></div>
    <input type="text" value="">
    <script>
        function setValue(selector, value) {
            if (document.querySelector(selector)) {
                document.querySelector(selector).textContent = value
            }
        }

        function render() {
            const value = document.querySelector('input')?.value
            const d = (Number(value) || 0) * 2.54
            const { width, height } = screen
            const sidesRatio = width / height
            const physicalWidth = Math.sqrt(d ** 2 / (1 + sidesRatio ** 2)) * sidesRatio
            const physicalHeight = Math.sqrt(d ** 2 / (1 + sidesRatio ** 2))
            let pixelsPerCm = height / physicalHeight

            if (pixelsPerCm === Infinity) {
                pixelsPerCm = 0
            }

            document.body.style.setProperty('--pixes-per-cm', `${pixelsPerCm}px`)
            setValue('.resolution', `${screen.width}x${screen.height}`)
            setValue('.sizes', `${physicalWidth.toFixed(2)} см x ${physicalHeight.toFixed(2)} см`)
            setValue('.pxs-per-cm', pixelsPerCm.toFixed(2))
        }

        setInterval(render, 200)
    </script>
</body>
</html>

Не стесняемся. Открываем страницу, вводим диагональ и замеряем красивый красный квадратик линейкой.

Ну и я замерю. Прекрасные фото красивого красного квадратика 3 × 3 см. Мечта. Фото с моего зоопарка прилагаю:

Я сделаль!

Получил свой желанный красивый красный квадратик с заданными характеристиками. Пусть и с большой оговоркой, но задачу я решил.

Отдельно скажу про два последних скрина. На первом — Firefox без зума, а на втором — Firefox с зумом 110% (мне так удобнее) отсюда и изменение разрешения. Тем не менее красивый красный квадратик как был 3 × 3 см, так и остался. Ширина поля ввода — 3 виртуальных сантиметра, и его размер «плавает» от устройства к устройству.

Теперь вы знаете, что можно подружить физические и виртуальные сантиметры. Может, это даже вам пригодится. Если вы сталкивались с подобной задачей или знаете, где можно применить данное нетайное знание, пишите в комментариях.

Комментарии (34)


  1. zabanen2
    04.10.2024 14:38
    +2

    мне бывает нужно, чтобы 2 графика имели одинаковый масштаб по x и y, при этом размеры axes у них в см одинаковые, при этом сам размер картинки тоже одинаковый. В excel такое сделать невозможно, только если скопировать картинку и туда вставить новые данные и молиться, чтобы ничего не сдвинулось. в matplotlib хоть и есть кое какой инструмент, приходится подбирать и писать громоздкую обертку. пока другие работают дальше и не парятся, я борюсь за красоту графиков


    1. fivlabor
      04.10.2024 14:38
      +2

      Не понял про Excel, там можно установить числами ширину объектов, мин-макс осей, шаг основных-вспомогательных делений и т.п. Или вы о том, что это приходится каждый раз снова вводить, а сохранить в шаблоне как-то мудренно?


    1. Andy_U
      04.10.2024 14:38

      в matplotlib хоть и есть кое какой инструмент, приходится подбирать и писать громоздкую обертку.

      Если код по ссылке https://matplotlib.org/stable/gallery/axes_grid1/demo_fixed_size_axes.html решает вашу задачу, то разве от громоздкий? Ну и несколько еще вариантов тут: https://stackoverflow.com/questions/44970010/axes-class-set-explicitly-size-width-height-of-axes-in-given-units


  1. Lovell_Loreley
    04.10.2024 14:38

    1) рисуешь 6*3

    2) Убираешь 3*3


  1. anonymous
    04.10.2024 14:38

    НЛО прилетело и опубликовало эту надпись здесь


  1. ImagineTables
    04.10.2024 14:38
    +1

    Как по мне, хорошее приложение не состоит из HTML'я целиком, а опирается на всю мощь нативного кода под капотом. Например, обращается к EDID. (Вот пример для использования в Windows-сборках, интересно, есть ли кросс-платформенный эквивалент). Один раз пришлось делать нечто отдалённо похожее, в CSS я использовал нестандартную единицу, привязанную к физическим размерам, а в скриптах — функцию, опирающуюся на kx и ky, значения которым присваивал из нативной части при запуске приложения.


    1. artptr86
      04.10.2024 14:38
      +5

      Только вот веб-приложению выполнить нативный код никто не даст


      1. equeim
        04.10.2024 14:38
        +2

        Да еще и EDID бывает кривой. Лучше уж спрашивать у ОС, у них свои костыли на этот случай есть.


      1. ImagineTables
        04.10.2024 14:38
        +1

        Да, бывает, что и такое надо кровь из носу. Например, приложение не проходит чёртову цензуру в AppStore. Там и тригонометрию вспомнишь, и всё на свете.

        Про тригонометрию это не совсем шутка. Я решал такую задачу: в чистом веб-аппе сделать строго ландшафтную ориентацию, независимо от поворота и блокировки экрана в ОС. При всей простоте постановки задачи закончилось тем, что я внутрь пол-учебника геометрии запихал. (Деталей уже не вспомню, это были времена iPhone 4).


  1. nin-jin
    04.10.2024 14:38

    1. johnfound
      04.10.2024 14:38
      +2

      У меня всегда window.devicePixelRatio == 1. А еще точнее, текущий зуум страницы / 100.


      1. nin-jin
        04.10.2024 14:38

        Поздравляю, у вас логический пиксель равен физическому.


        1. johnfound
          04.10.2024 14:38

          В том то и дело, что нет. Ну не может на 3 разных монитора размер пикселей быть одинаковым.


          1. bBars
            04.10.2024 14:38
            +1

            Физический размер к этому показателю не имеет отношения. Значение 1 означает, что выбранное разрешение для видяхи (логистический пиксель) в точности соответствует разрешению дисплея (физический пиксель — который светится). А отличаться от 1 оно будет в том случае, если на дисплее, скажем, 1024х768 выводить изображение другого разрешения


            1. johnfound
              04.10.2024 14:38

              Я этого и имел ввиду. Но это никак не поможет нарисовать квадрат с точными физическими размерами.


            1. Mangus323
              04.10.2024 14:38

              Значение 1 означает, что для CSS media query реальное разрешение экрана будет применяться также и для квери. Например, для типичного телефона с разрешением 1080x1920 devicePixelRatio == 2.75, и CSS будет воспринимать это разрешение как 393x721 и соответственно показывать мобильный вариант страницы. Ничего общего с физическими пикселями и дисплеем это значение не имеет


        1. vanxant
          04.10.2024 14:38
          +3

          Осталось узнать физический размер физического пикселя. И поскольку он будет в линиях (1/12 пункта pt, который 1/12 от дюйма, но это неточно), ещё нужно будет умножить на магическую константу, которая в Китае в пи раз выше, чем в Европе.


    1. sfi0zy
      04.10.2024 14:38
      +20

      Свойство devicePixelRatio - это отношение количества реальных пикселей на пиксель в CSS. Если оно больше 1, то мы можем понять, что у нас экран с большой плотностью пикселей. Это может быть полезно знать, чтобы пользователям грузить картинки или рендерить канвас в высоком разрешении. Но это не имеет никакого отношения к физическому размеру пикселей. Количество и размер - вещи перпендикулярные. Поэтому в контексте задачи это свойство никак не поможет узнать реальный размер элементов.


      1. zompin Автор
        04.10.2024 14:38
        +1

        Я опустил этот момент. Но да, devicePixelRatio не здесь не помощник


  1. PaulZi
    04.10.2024 14:38
    +10

    Краткое содержание:

    Как точно нарисовать квадрат 3х3? Попросить пользователя линейкой измерить диагональ и ввести в поле. А мы знаем супер формулу.

    В общем математика - сила, а статья... ну такая.


    1. 1dNDN
      04.10.2024 14:38
      +2

      Попросить пользователя вспомнить/погуглить самую известную характеристику дисплея и ввести в поле как привязку реального мира к виртуальному. Это другое


  1. boojum
    04.10.2024 14:38

    Так и не увидел в статье ни одного трёхсантиметрового квадрата.

    Что помешало привести в статье результат - тот самый красный квадрат 3 см?

    А ваш "Его величество код" нарисовал у меня вот такое.

    В общем, не зачёт.


    1. zompin Автор
      04.10.2024 14:38
      +6

      Подход основан на диагонали экрана. У вас диагональ не указана


    1. vabka
      04.10.2024 14:38

      Как показать, что ты невнимательно читал, не говоря, что ты невнимательно читал)


  1. Sadok
    04.10.2024 14:38
    +3

    "Мысью по древу", "мысью"...


  1. StjarnornasFred
    04.10.2024 14:38

    А можно ссылку?


  1. sogarkov
    04.10.2024 14:38

    А для принтеров в далёком 20 веке придумали постскрипт


  1. killyself
    04.10.2024 14:38
    +4

    Как этот код отработает с экраном 2×2 см?


    1. landybisquit
      04.10.2024 14:38
      +12

      И другие анекдоты из серии тестировщика в баре


    1. rendov
      04.10.2024 14:38

      Еще про совместимость с Internet Explorer 6 вообще забыли, где полифиллы на всякие флексы и клампы?