Сайт Романа ПарпалакаБлогКлючевые словаupmath

upmath

Upmath — это редактор математических текстов для интернета. Оформляете текст в маркдауне, пишете формулы на латехе, а Upmath выдает html-код для публикации в вебе.

Статьи по этой теме:
Латех и веб-технологии


Нативное gzip-сжатие в JS

1 июля 2025 года, 12:46

Я недавно закрыл тикет на гитхабе, который висел с 2017 года. Его автор обращал внимание на длинные адреса картинок в моем сервисе математических формул. С 2023 года нативное сжатие произвольных данных в JS стало доступным во всех основных браузерах, и с его помощью я сделал вариант сжатых адресов.

Давние читатели вспомнят, что я уже рассказывал об адресах картинок, и даже упоминал об этом тикете. Повторю, что для использования в вебе формулы, например, $$a^2+b^2=c^2$$, её исходник на латехе a^2+b^2=c^2 кодируется через проценты (RFC 3986) и подставляется в URL:

//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2

Кодирование через символы процента очень неэкономное, поэтому и без того длинный код изображений и диаграмм становится ещё больше. Адрес $$a^2+b^2=c^2$$ в новой схеме выглядит так:

//i.upmath.me/svgb/S4wz0k6KM7JNjjMCAA

Здесь вместо кодирования через проценты используется сжатие deflate (тот же алгоритм, что и в gzip) и кодировка, аналогичная base64. Вот рабочий пример кода, который делает такое преобразование:

function deflateRaw(text, callback) {
    if (typeof CompressionStream === 'undefined') {
        callback(null);
        return;
    }

    try {
        var stream = new Blob([text]).stream();
        var compressedStream = stream.pipeThrough(new CompressionStream('deflate-raw'));

        new Response(compressedStream).blob().then(function (compressedBlob) {
            return compressedBlob.arrayBuffer();
        }).then(function (buffer) {
            var compressedArray = new Uint8Array(buffer);
            var binary = Array.from(compressedArray).map(function (b) {
                return String.fromCharCode(b);
            }).join('');
            var base64 = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
            callback(base64);
        }).catch(function () {
            callback(null);
        });
    } catch (e) {
        callback(null);
    }
}

function getImgPath(formula, callback) {
    var fallbackUrl = '//i.upmath.me/svg/' + encodeURIComponent(formula);

    deflateRaw(formula, function (compressed) {
        var shortUrl = compressed ? '//i.upmath.me/svgb/' + compressed : null;
        callback(shortUrl && shortUrl.length < fallbackUrl.length ? shortUrl : fallbackUrl);
    });
}

Важная особенность API браузеров по сжатию заключается в его асинхронности. Мы не можем получить результат сжатия в той же функции, в которой его инициируем. API возвращает promise, который «разрешится» позднее. Чтобы обеспечить обратную совместимость и откатываться к несжатым адресам в старых браузерах, я проверяю саму поддержку CompressionStream и перехватываю возможные исключения. Также для обратной совместимости результат возвращаю через вызов пользовательского коллбэка, а не в виде промиса.

Вот пример того, как с вышеприведенным кодом создать картинку с формулой:

var node = document.createElement('img');
getImgPath('a^2+b^2=c^2', function(path) {
    node.setAttribute('src', path);
});

Стоит отметить, что сам алгоритм сжатия deflate был давно портирован на JS, поэтому при необходимости можно было использовать сторонние библиотеки, например, pako. Кроме того, код библиотек работает синхронно, так что ни о каких коллбэках и промисах думать не нужно. В моём же случае я не хотел в мини-скрипт по конвертации формул в картинки добавлять реализацию алгоритма сжатия на десяток-другой килобайт, тем более оставался обходной путь для старых браузеров с несжатыми адресами.

Оценим результат на примере диаграммы из предыдущей заметки. Длина старого несжатого URL равна 6,3 килобайт, а сжатого — 1,3 килобайт, что почти в 5 раз короче.

    2 комментария

Сложение вращений и анимация в TikZ

29 мая 2025 года, 12:35

В рекомендациях ютуба мне часто попадалась задачка о вращающихся окружностях. Вот её формулировка: окружность катится без проскальзывания по другой окружности втрое большего радиуса и совершает вокруг неё один оборот. Сколько оборотов при этом она совершит вокруг своего центра?

Эта задача встречалась в американском тесте абитуриентов 1982 года и примечательна тем, что среди предложенных вариантов ответов не было правильного. Сама задача, её история и связанные вопросы разобраны в этом видео:

Я вспомнил об этой задаче, потому что, наконец, разобрался как делать анимации в TikZ, и теперь вместо множества слов могу просто показать анимированные иллюстрации.

В этой задаче кажущийся ответ — три оборота — неправильный. Оборотов будет на 1 больше, чем отношение радиусов. В этом можно убедиться напрямую, просто подсчитав количество оборотов. Я сомневаюсь, что кто-то из читателей будет вырезать круги из картона и обкатывать один круг другим, поэтому давайте смотреть на анимацию. Здесь окружности заменены на «шестеренки», чтобы отсутствие проскальзывания было особенно наглядным:

$$\dvisvgm\definecolor{cyan}{RGB}{0, 200, 250} \shorthandoff{"} \usetikzlibrary {shapes.geometric} \usetikzlibrary{animations} \begin{tikzpicture} \def\a{1} \def\b{3} \useasboundingbox (-\b-2*\a-0.1,-\b-2*\a-0.1) rectangle (\b+2*\a+0.1,\b+2*\a+0.1); \draw[cyan,very thin] (-\b-2*\a,-\b-2*\a) grid (\b+2*\a,\b+2*\a); \node[star,star points=57, star point ratio=1.07,minimum size=6.2cm, draw,fill=white] at (0,0); \draw[purple,fill] (0:\b) circle (1pt) -- (0,0) circle (1pt) node [midway, sloped, above] {$\b$} ; \begin{scope}:rotate = {0s="0", (5*\b)s="360",repeats} \begin{scope} :rotate = {0s="0", (5*\a)s="360", origin={(\b+\a,0)}, repeats} \node [star,star points=19, star point ratio=1.2,minimum size=2.2cm, draw,fill=white] at (0:\b+\a) {}; \draw [purple,fill] (0:\b+\a) circle (1pt) -- (0:\b) circle (1pt) node [midway, sloped, above] {$\a$} ; \end{scope} \end{scope} \coordinate (A) at (1,1.5); \node [fill=white,inner sep=1pt,anchor=east,xshift=3pt,yshift=-1pt] at (A) {$\text{обороты: }\,\,\,.$}; \foreach \t in {3,2,...,0} { \node :opacity = { 3.75*(0) s="0", 3.75*(0+\t) s="0", 3.75*(0.001+\t) s="1", 3.75*(0.999+\t) s="1", 3.75*(1+\t)s="0", 3.75*(4) s="0", repeats } [anchor=east,inner sep=1pt] at (A) {$\t$}; } \foreach \t in {0,1,...,9} { \node :opacity = { 0.375*(0) s="0", 0.375*(0+\t) s="0", 0.375*(0.01+\t) s="1", 0.375*(0.99+\t) s="1", 0.375*(1+\t)s="0", 0.375*(10) s="0", repeats } [anchor=west,inner sep=1.5pt] at (A) {$\t$}; } \end{tikzpicture}$$

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

Представим оборот меньшей окружности вокруг большей как сумму двух движений. В первом движении окружности вращаются так, что их центры остаются неподвижными. В этом случае малая окружность действительно совершит три оборота, пока большая окружность совершает один оборот. Второе движение — это вращение обеих окружностей, соприкасающихся в одной точке, на ещё один оборот. При сложении двух движений обороты большой окружности оказываются разнонаправленными и компенсируются, а обороты малой окружности — однонаправленными и суммируются, то есть малая окружность совершит четыре оборота. На следующей иллюстрации этой идеи легко подсчитать количество оборотов каждой окружности в полном цикле:

$$\dvisvgm\definecolor{cyan}{RGB}{0, 200, 250} \usetikzlibrary {shapes.geometric} \usetikzlibrary{animations} \begin{tikzpicture} \def\a{1} \def\b{3} \useasboundingbox (-\b-2*\a-0.1,-\b-2*\a-0.1) rectangle (\b+2*\a+0.1,\b+2*\a+0.1); \draw[cyan,very thin] (-\b-2*\a,-\b-2*\a) grid (\b+2*\a,\b+2*\a); \begin{scope}:rotate = {0s="0", 5s="-360", 6s="-360", 11s="0", 12s="0", repeats} \node[star,star points=57, star point ratio=1.07,minimum size=6.2cm, draw,fill=white] at (0,0); \draw[purple,fill] (0:\b) circle (1pt) -- (0,0) circle (1pt) node [midway, sloped, above] {$\b$} ; \end{scope} \begin{scope} :rotate = {0s="0", 6s="0", 11s="360", 12s="360", origin={(0,0)}, repeats} \begin{scope} :rotate = {0s="0", 5s="1080", 6s="1080", 11s="1080", 12s="1080", origin={(\b+\a,0)}, repeats} \node [star,star points=19, star point ratio=1.2,minimum size=2.2cm, draw,fill=white] at (0:\b+\a) {}; \draw [purple,fill] (0:\b+\a) circle (1pt) -- (0:\b) circle (1pt) node [midway, sloped, above] {$\a$} ; \end{scope} \end{scope} \end{tikzpicture}$$

    Оставить комментарий

RSS, формулы и Feedly

26 сентября 2023 года, 23:22

Постоянные читатели помнят, что у меня есть сервис по превращению математических формул в картинки и редактор для создания математических текстов. С заменой формул на картинки на обычных веб-страницах проблем нет, вопрос в том, как поступать с RSS?

Еще в 2011 году я стал отдавать в RSS растровые картинки, потому что непонятно, в каком окружении будет отображаться контент оттуда. А на обычных веб-страницах js-код определял, есть ли в браузере поддержка SVG, и в этом случае подключал красивые векторные картинки.

Когда в 2013 году прекратила работать rss-читалка Google Reader, народ в основном стал использовать Feedly. Напомню, что тогда Feedly работал через API Google Reader. Они подсуетились и написали свой бэкенд, чтобы не только не растерять свою аудиторию, но и подхватить доставшуюся даром аудиторию сервиса гугла.

У Feedly была одна особенность с отображением картинок: они принудительно становились плавающими (включалось css-свойство float). Если в тексте встречается одна иллюстрация, это может быть нормально. Но математические тексты с обилием формул превращались в нечитаемую кашу. Я написал им в поддержку, никакого ответа не получил. И мне пришлось отключить преобразование формул в картинки: я отдавал вместо формул вроде $$E=mc^2$$ их исходный код $$E=mc^2$$.

Сейчас я решил проверить, не научился ли Feedly отображать нормально картинки. Оказалось, что научился. Если вы читаете этот текст через RSS в Feedly, можете в этом убедиться в предыдущем абзаце.

Чтобы два раза не вставать, я попросил ChatGPT составить xslt-преобразование для xml-кода rss-канала, которое бы облагородило внешний вид при открытии RSS в браузере. Я оставил по ссылке нейтральный стиль, но никто не мешает использовать CSS с фирменным дизайном. У меня получился такой xslt-файл:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="/">
        <html>
            <head>
                <title><xsl:value-of select="rss/channel/title" /></title>
                <style type="text/css">
                    a {
                        color: #56d;
                        text-decoration-thickness: 1px;
                        text-decoration-color: rgba(85, 102, 221, 0.5);
                    }
                    body {
                        max-width: 720px;
                        font: 16px/1.5 sans-serif;
                        margin: 0 auto;
                    }
                    h1, h2, h3 {
                        margin: 1em 0 0.25em;
                    }
                    p {
                        margin: 0 0 0.75em;
                    }
                </style>
            </head>
            <body>
                <h1>
                    <a href="{rss/channel/link}">
                        <xsl:value-of select="rss/channel/title" />
                    </a>
                </h1>
                <xsl:for-each select="rss/channel/item">
                    <div class="item">
                        <h2><a href="{link}"><xsl:value-of select="title" /></a></h2>
                        <div><xsl:value-of select="description" disable-output-escaping="yes" /></div>
                    </div>
                </xsl:for-each>
            </body>
        </html>
    </xsl:template>
</xsl:stylesheet>

Чтобы это заработало, нужно добавить ссылку на такой xslt-файл в RSS:

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/_styles/rss.xslt" type="text/xsl"?>
<rss version="2.0">
    <channel>
	...
    </channel>
</rss>

Кстати, пока со всем этим возился, обнаружил, что в RSS сломалось отображение исходного кода: перестало работать сохранение переносов строк внутри тегов <pre><code>. Долго пытался понять, где ошибка, пока не осознал, что это сломался сам Feedly. Похоже, эта проблема наблюдается в любых источниках в Feedly за последние недели две. Заодно сейчас и проверим, нормально ли будет отформатирован код в этой заметке.

В заключение полезный совет: если хотите отладить RSS и не хотите ждать, пока Feedly проиндексирует новую запись, можете на экране добавления нового источника добавить в URL не меняющую смысла часть запроса. Тогда Feedly распарсит RSS на лету и отобразит три последних записи:

Добавлено позднее: оказывается, Feedly нормально отображает картинки только в веб-версии. В приложении на андроиде формулы ни в каком варианте не отображаются, не работает ни SVG и PNG в тегах img, ни SVG, добавленный напрямую в HTML. Буду считать, что это баг на их стороне, и ничего с этим делать не буду.

    4 комментария

Воспринимайте слова в переписке буквально

19 сентября 2020 года, 14:23

В общении между людьми есть проблема недопонимания. Говорящий не всегда правильно подбирает слова, а слушающий додумывает. Иногда получается, что имели в виду одно, сказали другое, а поняли третье.

В устном общении есть возможность задать уточняющий вопрос, и проблема часто снимается. В переписке с этим сложнее, поэтому особенно важно следить за словами: проверять, правильно ли слова передают смысл, и воспринимать слова буквально.

Смотрите, что получается, если кто-то в переписке не придерживается таких правил. Мне написал посетитель сервиса для генерации картинок с формулами. В письме с темой «Про отсутствие https в адресах картинок на tex.s2cms.ru/» он сформулировал мысль так:

Во всех адресах и примерах использования «https:» почему-то исчезло. Так что адреса формально неправильные.

Чтобы не отправлять вас по ссылкам, поясню, что сервис предлагает УРЛы картинок в виде //i.upmath.me/svg/f(x) и код встраивания скрипта <script src="//i.upmath.me/latex.js"></script>.

Дело в том, что схема (http или https для веба) — необязательная часть адреса, его можно опустить (см. network-path reference в RFC). Тогда подразумевается, что используется текущая схема. Независимо от того, включен ли на сайте https или нет, картинки и скрипт загрузятся по соответствующей схеме.

Посетитель вложил в свою мысль целых два неверных утверждения. Первое: https куда-то исчезло. Это неправда, https никогда не фигурировал в адресах. Второе: адреса формально неправильные. Это тоже неправда, ссылку на RFC я выше привел.

Если бы посетитель спросил, почему так сделано, я бы ответил, и может быть даже погуглил за него. Но вопроса не было, поэтому в своем ответе я задал уточняющие вопросы:

Это вопрос или предложение? Почему вы считаете, что «https:» исчезло?

Первая часть — отсылка к технике продуктивной переписки «общаться вопросами и предложениями». Непонятно, интересовался ли посетитель, почему так сделано, или предлагал сделать по-другому. Эту часть вопроса он проигнорировал. На вторую часть он ответил «Смотрите сами» и приложил скриншоты с подчеркнутыми УРЛами без схемы. Разумеется, схемы в них нет, я сделал это специально. Вопроса в сообщении опять не было. Как и задачи у меня кого-либо переубедить. Поэтому переписка не продолжилась.

Закрепим рекомендации:

  1. Когда вы пишете сообщение в мессенджере или почте, перечитайте перед отправкой и проверьте, что сообщение отражает вашу мысль.
  2. Когда вы получили сообщение, воспринимайте его буквально, не додумывайте за автора. Если автору нужно было другое, он переспросит.
  3. Отвечайте на явно сформулированные вопросы. Прокомментируйте предложения: будем ли мы делать то, что предлагается, и если нет, то почему.
  4. Если ни вопросов, ни предложений нет, а от вас требуется реакция, и вы не знаете, что ответить, так и напишите: «Я не понимаю, в чем твой вопрос и что ты предлагаешь».
    Оставить комментарий

Редактор математических текстов Mathcha

13 сентября 2020 года, 18:11

Искал онлайн-инструменты для редактирования картинок TikZ и наткнулся на редактор Mathcha.

Этот редактор — визуальный: вы сразу редактируете документ вместе с форматированием. В отличие от моего редактора Upmath, в котором вы редактируете исходник на маркдауне и латехе, хотя и сразу видите результат.

Вот рисунок, который я сделал с помощью Mathcha. Накидал основу в нем, экспортировал в TikZ и подправил исходный код уже в UpMath.

$$ \tikzset{every picture/.style={line width=0.75pt}} %set default line width to 0.75pt \begin{tikzpicture}[x=0.75pt,y=0.75pt,yscale=-1,xscale=1] %uncomment if require: \path (0,300); %set diagram left start at 0, and has height of 300 %Shape: Boxed Line [id:dp7642567693966007] \draw (130,80) -- (130,140) ; %Shape: Boxed Line [id:dp02318954146147889] \draw (130,150) -- (130,180) ; %Shape: Boxed Line [id:dp4638027067588357] \draw (130,190) -- (130,250) ; %Shape: Wave [id:dp03622557580885122] \draw [color={rgb, 255:red, 74; green, 144; blue, 226 } ,draw opacity=1 ] (215,80) .. controls (202.19,83.1) and (190,86.06) .. (190,89.5) .. controls (190,92.94) and (202.19,95.9) .. (215,99) .. controls (227.81,102.1) and (240,105.06) .. (240,108.5) .. controls (240,111.94) and (227.81,114.9) .. (215,118) .. controls (202.19,121.1) and (190,124.06) .. (190,127.5) .. controls (190,130.94) and (202.19,133.9) .. (215,137) .. controls (227.81,140.1) and (240,143.06) .. (240,146.5) .. controls (240,149.94) and (227.81,152.9) .. (215,156) .. controls (202.19,159.1) and (190,162.06) .. (190,165.5) .. controls (190,168.94) and (202.19,171.9) .. (215,175) .. controls (227.81,178.1) and (240,181.06) .. (240,184.5) .. controls (240,187.94) and (227.81,190.9) .. (215,194) .. controls (202.19,197.1) and (190,200.06) .. (190,203.5) .. controls (190,206.94) and (202.19,209.9) .. (215,213) .. controls (227.81,216.1) and (240,219.06) .. (240,222.5) .. controls (240,225.94) and (227.81,228.9) .. (215,232) .. controls (202.19,235.1) and (190,238.06) .. (190,241.5) .. controls (190,244.57) and (199.7,247.26) .. (210.89,250) ; %Straight Lines [id:da008457960885399407] \draw (190,80) -- (190,250) ; %Flowchart: Summing Junction [id:dp09695643217509597] \draw (135,132.75) .. controls (135,128.75) and (138.36,125.5) .. (142.5,125.5) .. controls (146.64,125.5) and (150,128.75) .. (150,132.75) .. controls (150,136.75) and (146.64,140) .. (142.5,140) .. controls (138.36,140) and (135,136.75) .. (135,132.75) -- cycle ; \draw (137.2,127.62) -- (147.8,137.88) ; \draw (147.8,127.62) -- (137.2,137.88) ; %Shape: Inductor [id:dp3454355331692156] \draw (35,155) -- (42.06,155) .. controls (43.68,155) and (45,156.12) .. (45,157.5) .. controls (45,158.88) and (43.68,160) .. (42.06,160) .. controls (43.68,160) and (45,161.12) .. (45,162.5) .. controls (45,163.88) and (43.68,165) .. (42.06,165) .. controls (43.68,165) and (45,166.12) .. (45,167.5) .. controls (45,168.88) and (43.68,170) .. (42.06,170) .. controls (43.68,170) and (45,171.12) .. (45,172.5) .. controls (45,173.88) and (43.68,175) .. (42.06,175) -- (35,175) ; %Straight Lines [id:da4690178114375858] \draw [dash pattern={on 0.84pt off 2.51pt}] (45,165.5) -- (130,185) -- (190,165) ; %Straight Lines [id:da10916309009890135] \draw [dash pattern={on 0.84pt off 2.51pt}] (45,164.5) -- (130,145) -- (190,165) ; % Text Node \draw (112,127) node [anchor=north west][inner sep=0.75pt] [align=left] {A}; % Text Node \draw (112,191) node [anchor=north west][inner sep=0.75pt] [align=left] {B}; \end{tikzpicture} $$

    Оставить комментарий

Кеширование в nginx

28 августа 2020 года, 22:12

В прошлый раз мы рассмотрели, как в теории работает кеш и какие ошибки обычно совершают при его программировании. В этот раз я расскажу, как с помощью нескольких настроек nginx включить кеширование прямо на уровне веб-сервера, избежав при этом программирования и подводных камней вроде условия гонки. Но сначала опишу проблему, которую решал.

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

Иногда папку с кешем приходится удалять, если она слишком сильно разрослась. Или если я вношу правки в систему, и кеш устаревает. Посетители популярных страниц генерируют множество запросов к одинаковым формулам. В кеше их нет. Nginx направляет запросы к PHP. PHP на каждую формулу вызывает консольный скрипт латеха. Раньше у меня не было защиты от того, чтобы сервер в нагруженном состоянии хотя бы не делал одно и то же много раз. Это классическое условие гонки.

Как оказалось, приемлемое решение — включить кеш в nginx и настроить блокировку. Тогда он пропускает на бэкенд разные запросы, а одинаковые выстраивает в очередь ожидания. Результат записывает в свой внутренний временный кеш и отдает всем ожидавшим клиентам.

В блоке конфигурации http указываем папку и другие параметры зоны кеша:

fastcgi_cache_path /var/data/i.upmath.me levels=1:2 keys_zone=i_upmath:10m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

Обратите внимание на ключ кеширования. Я использовал обычный адрес ресурса, потому что картинки публичны и одинаковы для всех. Если у вас закрытые ресурсы, можете попробовать добавить куки в ключ. Хотя я бы не стал так делать: велик риск ошибки и утечки чужих приватных данных через кеш.

Далее в нужном location подключаем зону:

fastcgi_cache i_upmath;
fastcgi_cache_valid 200 10m;
fastcgi_cache_methods GET HEAD;
fastcgi_cache_lock on;
fastcgi_cache_lock_age 9s;
fastcgi_cache_lock_timeout 9s;

Помимо кеширования здесь включена блокировка для предотвращения race condition. Длительность блокировки и ожидания я выбрал 9 секунд, потому что таймаут запуска латеха в моей системе 8 секунд. Вы можете подобрать другое значение.

На моем сервере ограниченное количество процессов php-fpm могут генерировать картинки. Это сделано, чтобы не мешать другим сайтам. Чтобы после очистки кеша запросы к картинкам дожидались их генерации, я увеличил таймауты nginx для ожидания бэкенда:

fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
fastcgi_connect_timeout 90;
fastcgi_send_timeout 90;

Технически правильное решение в моей ситуации — прогревать новый кеш, пока система работает со старым. А именно, брать часть текущего потока запросов формул, генерировать для них новые картинки и складывать в новую папку. Когда популярные формулы окажутся в новом кеше, переключать папки.

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

    Оставить комментарий

Стили для печати и конвертация в PDF

26 июля 2020 года, 16:16

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

От пользователя пришло письмо, в котором он спрашивает, есть ли конвертация маркдауна и латеха в PDF. Вопрос задают не первый раз. Мне сообщали о каких-то утилитах вроде pandoc. Я отвечал, что программировать это не буду.

Главная задача сервиса Upmath — подготовка математических текстов для публикации в вебе. Результат его работы — html-код. Если кому-нибудь нужен PDF, его можно получить с помощью самого латеха. На выходе будет превосходно сверстанный документ со всеми типографскими плюшками.

Но сейчас я задумался. Не всем нужен высококачественный PDF. Кто-то готовит текст самостоятельной работы для учеников и студентов. Кто-то распечатывает черновики, чтобы править ошибки. Что мешает получить PDF прямо сейчас?

Печатать можно напрямую из браузера. Хоть на настоящем принтере, хоть в pdf-документ. Я до сих пор не задумывался об этом, и не подготовил стили для печати (раньше вообще svg-картинки нормально не печатались). Получалась бесполезная страница с началом текста и его исходником:

Мне ничего не стоило добавить стили для печати. Результат сразу преобразился:

Качество получающихся документов мне не очень нравится. Я добился того, чтобы картинки не разбивались на две страницы. Но исключить разрыв страницы после заголовков у меня не получилось. Не понимаю, почему ни хром, ни FF не понимают инструкцию

h1, h2, h3 {
    break-after: avoid;
}

Будем ждать, пока эти баги в браузерах будут исправлены.

    Оставить комментарий

О схеме URL сервиса генерации картинок с формулами

6 января 2018 года, 01:13

В посте про объемный чертеж я привел саму картинку, но не ее код. А без кода картинка бесполезна: нельзя ни подсмотреть, как она сделана, ни изменить под свои нужды.

На самом деле код картинки содержится в ее адресе. Как писал Якоб Нильсен 19 лет назад, URL — это интерфейс. И я принял такое интерфейсное решение. Формула на латехе (например, a^2+b^2=c^2) кодируется и добавляется в урл:

//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2

По нему открывается сама картинка с формулой: $$a^2+b^2=c^2$$.

Таким образом, из адреса картинки расшифровкой можно получить ее исходный код. Чтобы облегчить этот процесс, я сделал адрес другого вида (от svg остается последний символ):

//i.upmath.me/g/a%5E2%2Bb%5E2%3Dc%5E2

По этой ссылке открывается веб-интерфейс генератора картинок с заполненным исходным кодом. Вы меняете код и сразу видите результат:

Вообще, исходный код может быть достаточно длинным. Вот пример, в котором на комплексной плоскости отмечены первые 10 степеней числа $$1+i\pi/10$$:

\begin{tikzpicture}[scale=1.0545]\small
\tikzset{>=stealth}
\def\k{10}
\def\p{3.1415926/\k}
\def\r{3.1}
\def\l{5.8}
\def\t{0.07}
\draw[->,thin,gray] (-\l,0)--(\l,0);
\draw[->,thin,gray] (0,-0.6)--(0,\l);
\draw[green!40!black](\r,0) -- (\r,\p*\r) node[midway,right] {$i\pi/\k$};
\foreach \l in {1,...,\k}
   \draw[->] (0,0) -- ({(\l-1)*atan(\p)}:{((sqrt(1+\p*\p)^(\l-1)*\r)});
\draw[->,red] (0,0) -- ({\k*atan(\p)}:{((sqrt(1+\p*\p)^\k*\r)}) node[pos=0.91,above] {$-1,\!5934+0,\!1561i$};
\draw[very thin] (\r,\t)--(\r,-\t) node[below]{$1$}
(-\r,\t)--(-\r,-\t) node[below]{$-1$}
(\t,\r)--(-\t,\r) node[left]{$1$}
(0,0) node [anchor=north west,yshift=-0.07cm] {$0$};
\draw [line width=0.21mm,opacity=0] (-\l,-0.6) rectangle (\l,\l);
\end{tikzpicture}

Результат:

$$\begin{tikzpicture}[scale=1.0545]\small \tikzset{>=stealth} \def\k{10} \def\p{3.1415926/\k} \def\r{3.1} \def\l{5.8} \def\t{0.07} \draw[->,thin,gray] (-\l,0)--(\l,0); \draw[->,thin,gray] (0,-0.6)--(0,\l); \draw[green!40!black](\r,0) -- (\r,\p*\r) node[midway,right] {$i\pi/\k$}; \foreach \l in {1,...,\k} \draw[->] (0,0) -- ({(\l-1)*atan(\p)}:{((sqrt(1+\p*\p)^(\l-1)*\r)}); \draw[->,red] (0,0) -- ({\k*atan(\p)}:{((sqrt(1+\p*\p)^\k*\r)}) node[pos=0.91,above] {$-1,\!5934+0,\!1561i$}; \draw[very thin] (\r,\t)--(\r,-\t) node[below]{$1$} (-\r,\t)--(-\r,-\t) node[below]{$-1$} (\t,\r)--(-\t,\r) node[left]{$1$} (0,0) node [anchor=north west,yshift=-0.07cm] {$0$}; \draw [line width=0.2mm,opacity=0] (-\l,-0.6) rectangle (\l,\l); \end{tikzpicture}$$

Этот пример я взял из статьи о формуле Эйлера $$e^{i\pi}=-1$$. Первую версию картинки рисовал вручную. В Maple выполнил возведение в степень и построил график. Открыл его в фотошопе и поверх дорисовал стрелки-векторы, оси координат.

На версию с графиком в tikz ушло столько же времени, или даже больше. Но масштабируемость результата находится на совершенно другом уровне. Чтобы нарисовать первым способом вдвое больше векторов, нужно потратить вдвое больше времени. Второй способ требует изменения одного числа k в исходном коде, и foreach сделает всю работу. Вот 20 векторов:

$$\begin{tikzpicture}[scale=1.0545]\small \tikzset{>=stealth} \def\k{20} \def\p{3.1415926/\k} \def\r{3.1} \def\l{5.8} \def\t{0.07} \draw[->,thin,gray] (-\l,0)--(\l,0); \draw[->,thin,gray] (0,-0.6)--(0,\l); \draw[green!40!black](\r,0) -- (\r,\p*\r) node[midway,right] {$i\pi/\k$}; \foreach \l in {1,...,\k} \draw[->] (0,0) -- ({(\l-1)*atan(\p)}:{((sqrt(1+\p*\p)^(\l-1)*\r)}); \draw[->,red] (0,0) -- ({\k*atan(\p)}:{((sqrt(1+\p*\p)^\k*\r)}); \draw[very thin] (\r,\t)--(\r,-\t) node[below]{$1$} (-\r,\t)--(-\r,-\t) node[below]{$-1$} (\t,\r)--(-\t,\r) node[left]{$1$} (0,0) node [anchor=north west,yshift=-0.07cm] {$0$}; \draw [line width=0.2mm,opacity=0] (-\l,-0.6) rectangle (\l,\l); \end{tikzpicture}$$

Вот 50:

$$\begin{tikzpicture}[scale=1.0545]\small \tikzset{>=stealth} \def\k{50} \def\p{3.1415926/\k} \def\r{3.1} \def\l{5.8} \def\t{0.07} \draw[->,thin,gray] (-\l,0)--(\l,0); \draw[->,thin,gray] (0,-0.6)--(0,\l); \draw[green!40!black](\r,0) -- (\r,\p*\r) node[midway,right] {$i\pi/\k$}; \foreach \l in {1,...,\k} \draw[->] (0,0) -- ({(\l-1)*atan(\p)}:{((sqrt(1+\p*\p)^(\l-1)*\r)}); \draw[->,red] (0,0) -- ({\k*atan(\p)}:{((sqrt(1+\p*\p)^\k*\r)}); \draw[very thin] (\r,\t)--(\r,-\t) node[below]{$1$} (-\r,\t)--(-\r,-\t) node[below]{$-1$} (\t,\r)--(-\t,\r) node[left]{$1$} (0,0) node [anchor=north west,yshift=-0.07cm] {$0$}; \draw [line width=0.2mm,opacity=0] (-\l,-0.6) rectangle (\l,\l); \end{tikzpicture}$$

URL последней картинки длиной 1192 символа. В принципе, такие длинные адреса не очень красивы. Но и большой проблемы в этом нет. В стандарте нет ограничений на длину URL, да и с практическим ограничением в браузере или веб-сервере при штатном использовании сервиса я не сталкивался.

Меня просили изменить алгоритм кодирования, чтобы укоротить адреса. Необходимости в этом я пока не вижу. Но если буду делать доработку, то поступлю аналогично. Адрес /svgb/... с закодированными в base-64 бинарными данными будет вести на картинку, /gb/... на дешифрованный вариант в редакторе формул.

    Оставить комментарий

Объемный чертеж

3 января 2018 года, 13:48

Нарисовал в латехе объемный чертеж, но он не пригодился. Так что просто оставлю его здесь.

Чтобы делать такие чертежи, добавил в свой редактор математических текстов Upmath поддержку пакета tikz-3dplot. А еще в последнее время меня просили добавить пакеты stmaryrd (набор символов для информатики) и bussproofs (выводы доказательств).

Сравните с версий того же чертежа из черновика:

    Оставить комментарий

Синхронная прокрутка

6 ноября 2017 года, 16:18

Этот текст я написал в том числе и для себя, чтобы в следующий раз не попадать в ловушку «почему два года назад я сделал так криво, сейчас всё исправлю».

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

Пример синхронной прокрутки есть в каждом интерфейсе просмотра изменений файлов. Вот PhpStorm:

При прокрутке одной версии документа вторая всегда выравнивается так, чтобы на красной линии оказались соответствующие строки. Аналогичный способ я выбрал для своего редактора математических текстов, только ориентир выравнивания сделал посередине:

Здесь мы набираем исходный код и сразу же видим результат. Обратите внимание на неравномерность прокрутки. Слева картинка кодируется одной строкой, а справа она занимает почти всю высоту прокручиваемой области.

Такая неравномерность приводит к ряду проблем. Когда картинка добавляется последней строчкой, в предпросмотре отображается только ее верхушка:

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

Другая идея — сделать ориентир не фиксированным, а подвижным. Сначала ориентир расположен вверху. Затем, по мере прокрутки, он сдвигается вниз. Но и здесь нас ждут скользкие моменты. Например, мы знаем, что начало одного и того же абзаца находится в левом документе на высоте в 30%, а в правом на 50%. Пусть левый документ прокрутили на 30%. Тогда мы говорим, что ориентир тоже находится на 30% высоты экрана, переводим 30% в 50%, и располагаем правый документ так, чтобы абзац был на 30% высоты экрана. Проблема в том, что обратное преобразование не обязано давать тот же самый результат. Действительно, тот же самый абзац будет использоваться при прокрутке правого документа на 50%, и в общем случае это разные положения прокрутки. В такой реализации встречаются странные скачки прокрутки при переключении фокуса между документами и даже немонотонность зависимой прокрутки: если вы прокручиваете один документ вниз, то другой в некоторые моменты будет двигаться не вниз, а вверх.

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

Похоже, полностью избавиться от проблем синхронной прокрутки вообще нельзя. Вернемся к сравнению файлов. Предположим, в одном файле 100 строк, во втором я превратил сотую строку в еще 400. Прокручивая первый файл, мы никогда не увидим новые 400 строк второго. Как бы мы ни выравнивали второй файл, все 400 строк на экран не поместятся. Это значит, что нельзя убирать вторую прокрутку.

В своем редакторе я оставил синхронизацию прокрутки по ориентиру, но дополнил ее условием: если один документ полностью прокручен вверх или вниз, то второй документ прокручивается полностью.

    Оставить комментарий

Естественная анимация в интерфейсах

25 мая 2016 года, 10:50

Анимация в интерфейсе делает наглядным изменение его состояния. Например, при неудачной отправке длинная форма прокручивается к неправильно заполненному полю. Или увеличивающаяся по нажатию фотография раздвигает окружающие элементы.

Без анимации сложнее воспринимать резкие и внезапные изменения. Вместе с тем анимация должна быть короткой и ненавязчивой, чтобы не мешать пользователю.

Анимация кажется естественной, когда повторяет привычное движение предметов окружающего мира. Я расскажу, как делал анимацию в своем редакторе математических текстов Upmath на основе физических законов. Готовый результат в изолированном окружении смотрите на демо-странице, где один блок следует за другим при движении мыши.

Вспоминаем физику

Перемещение объектов описывается изменением координат x с течением времени t. Если вы попытаетесь подобрать функцию x(t) «на глазок», потратите много времени, добиваясь плавного и естественного движения. Что выбрать? Гиперболу? Параболу? Куда ее переместить? Как повернуть?

За примерами движения лучше всего обратиться к предметам окружающего мира. Математический закон их движения диктуется физикой. Толкнем брусок, лежащий на столе. Он проходит определенное расстояние, замедляясь под действием силы трения. В хорошем приближении сила сухого трения скольжения постоянна, и зависимость x(t) оказывается параболой. Такое замедление можно использовать, если в начальный момент объект анимации уже двигался.

$$\begin{tikzpicture} \def\t{0} \def\r{3.4} \begin{axis}[width=10cm,height=7cm, ticks=none, xmin=-0, xmax=3.8, axis y line=left,axis x line=bottom, xlabel=$t$,ylabel=$x$, every axis x label/.style={at={(current axis.south east)},anchor=south}, every axis y label/.style={at={(current axis.north west)},anchor=west}, enlargelimits=true,mark size=1 ] \addplot[smooth,blue,domain=\t:\r,samples=80]{-(x-\r)^2} node[pos=0.75,black,anchor=south east,inner sep=2pt]{$x=A+Bt+Ct^2$}; \addplot[dashed,domain=\r-0.7:\r,samples=2]{0}; \addplot[mark=*] coordinates {(\t,-\r*\r)}; \addplot[mark=*,green!50!black] coordinates {(\r,0)} node[pin=-90:{\footnotesize{\text{\sffamily плавная остановка :)}}}]{}; \end{axis} \end{tikzpicture}$$

Рис. 1. Торможение сухим трением по параболе

Сила вязкого трения пропорциональна скорости движения тела. В таком случае тело будет двигаться к точке остановки по экспоненте за бесконечно большое время. Если экспоненту исказить, чтобы ограничить время движения, такая анимация будет казаться неестественной. Из-за трудностей с остановкой в разумное время не следует использовать модель вязкого трения, только если симуляция самого вязкого трения не является целью.

$$\begin{tikzpicture} \def\t{0} \def\r{3.8} \begin{axis}[width=10cm,height=7cm, ticks=none, xmin=-0, xmax=3.8, axis y line=left,axis x line=bottom, xlabel=$t$,ylabel=$x$, every axis x label/.style={at={(current axis.south east)},anchor=south}, every axis y label/.style={at={(current axis.north west)},anchor=west}, enlargelimits=true,mark size=1 ] \addplot[smooth,blue,domain=\t:\r,samples=80] {1-exp(-x*1.0)} node[pos=0.45,black,anchor=south east,inner sep=2pt]{$x=A-Be^{-\alpha t}$}; \addplot[dashed,domain=\t:\r,samples=2]{1}; \addplot[mark=*] coordinates {(\t,0)}; \addplot[red!80!black] coordinates {(3.4,1)} node[pin=-90:{\footnotesize{\text{\sffamily не останавливается :(}}}]{} ; \end{axis} \end{tikzpicture}$$

Рис. 2. Торможение по экспоненте в вязкой среде

Отклоненный от положения равновесия маятник (или грузик на пружине) плавно набирает скорость, проходит положение равновесия и плавно тормозит. Затем движение повторяется в обратную сторону, и так до бесконечности (если трения нет). График такого движения — синусоида. Периодический повтор нам не особо интересен, а вот движение маятника между крайними точками получается плавным и естественным.

$$\begin{tikzpicture} \def\t{0} \def\r{3.1415} \begin{axis}[width=10cm,height=7cm, ticks=none,mark size=1, xmin=-0.5, xmax=3.6, axis y line=left,axis x line=bottom, xlabel=$t$,ylabel=$x$, every axis x label/.style={at={(current axis.south east)},anchor=south}, every axis y label/.style={at={(current axis.north west)},anchor=west}, enlargelimits=true ] \addplot[smooth,blue,domain=\t:\r,samples=80] {1-cos(deg(x))} node[pos=0.52,black,anchor=south east,inner sep=2pt]{$x=A-B\cos\omega t$}; \addplot[dashed,domain=\t:\t+0.6,samples=2] {1-cos(deg(\t))}; \addplot[dashed,domain=\r-0.6:\r,samples=2] {1-cos(deg(\r))}; \addplot[mark=*,green!50!black] coordinates {(\t,0)} node[pin=90:{\footnotesize{\text{\sffamily плавный запуск :)}}}]{}; \addplot[mark=*,green!50!black] coordinates {(\r,2)} node[pin=-90:{\footnotesize{\text{\sffamily\quad плавная остановка :)}}}]{}; \end{axis} \end{tikzpicture}$$

Рис. 3. Движение маятника по синусоиде между крайними точками

В JS-библиотеках и CSS есть заготовки easing-функций для создания специальных эффектов. Почти все заготовки следует использовать с осторожностью, тщательно проверяя их уместность. Только синусоида более-менее универсальна:

Обычно я выбираю длительность анимации по синусоиде в 200 миллисекунд. Такая длительность в несколько раз больше времени реакции человека. Анимация хорошо заметна, но не успевает раздражать.

Давайте научимся проводить синусоидальную траекторию по начальным условиям, времени движения и точке остановки.

Как провести синусоиду через две точки

Пусть тело покоится в начальный и конечный момент времени. Тогда касательные к графику в точках t1 и t2 горизонтальны, а сам график — это полупериод синусоиды.

$$\begin{tikzpicture} \def\t{0} \def\r{3.1415} \begin{axis}[ ticks=none, xmin=-1, xmax=4.5, axis y line=left,axis x line=bottom, xlabel=$t$,ylabel=$x$, every axis x label/.style={at={(current axis.south east)},anchor=south}, every axis y label/.style={at={(current axis.north west)},anchor=west}, enlargelimits=true ] \addplot[smooth,blue,domain=\t:\r,samples=80] {1-cos(deg(x))}; \addplot[dashed,domain=\t:\t+1,samples=2] {1-cos(deg(\t))}; \addplot[dashed,domain=\r-1:\r,samples=2] {1-cos(deg(\r))}; \addplot[mark=*,mark size=1] coordinates {(\t,0)} node[pin=95:{$(t_1,x_1)$}]{} ; \addplot[mark=*,mark size=1] coordinates {(\r,2)} node[pin=-85:{$(t_2,x_2)$}]{} ; \end{axis} \end{tikzpicture}$$

Рис. 4. График движения между двумя положениями покоя

Уравнение, описывающее полупериод синусоиды, легко подобрать:

$$x(t)=x_1+{x_2-x_1\over 2}\left[1 - \cos\left(\pi{t-t_1\over t_2-t_1}\right)\right].$$

После окончания одной анимации мы можем начинать другую опять по этой формуле. Но что делать, если новая анимация должна начаться, пока еще не закончилась старая? Чтобы обеспечить плавность движения, мы останавливаем текущую анимацию (синяя линия) и начинаем новую анимацию (красная линия) с ненулевой начальной скоростью:

$$\begin{tikzpicture} \def\tgnt{0.7} \def\t{0} \def\r{3.1415} \def\tb{1} \def\rb{\r+\tb} \def\dx{1.27} \begin{axis}[ ticks=none, xmin=-1, xmax=4.9, axis y line=left,axis x line=bottom, xlabel=$t$,ylabel=$x$, every axis x label/.style={at={(current axis.south east)},anchor=south}, every axis y label/.style={at={(current axis.north west)},anchor=west}, enlargelimits=true,mark size=1 ] \addplot[smooth,blue,domain=\t:\tb,samples=80] {-cos(deg(x))+1}; \addplot[smooth,dotted,blue,domain=\tb:\r,samples=10] {-cos(deg(x))+1}; \addplot[blue,dashed, domain=\t:\t+\tgnt,samples=2] {-cos(deg(\t))+1}; \addplot[smooth,thick,red,domain=\tb:\rb,samples=80] {-1.5*cos(deg(1+0.69*(x-\tb)))+\dx}; \addplot[dashed,red,domain=\tb-\tgnt:\tb+\tgnt,samples=2] {-1.5*cos(deg(\tb))+\dx+sin(deg(\tb))*(x-\tb)}; \addplot[dashed,red,domain=\rb-\tgnt:\rb,samples=2] {1.5+\dx}; \addplot[mark=*] coordinates {(\t,0)}; \addplot[mark=*] coordinates {(\tb,1-cos(deg(\tb)))} node[pin=-85:{$(t_1,x_1)$}]{} ; \addplot[mark=*] coordinates {(\r,2)}; \addplot[mark=*] coordinates {(\rb,1.5+\dx)} node[pin=-85:{$(t_2,x_2)$}]{} ; \end{axis} \end{tikzpicture}$$

Рис. 5. График движения с ненулевой начальной скоростью

Без математических вычислений не получится написать формулу, соответствующую красной линии. Давайте проделаем эти вычисления.

Семейство всех возможных синусоид описывается уравнением

$$f(t)=A\cos\omega (t-t_2)+B\sin\omega (t-t_2)+C$$(1)

с четырьмя неизвестными параметрами A, B, C и $$\omega>0$$. Я сдвинул начало отчета времени в точку t2, чтобы сразу избавиться от второго слагаемого. Действительно, производная $$f'(t_2)=B\omega$$ должна быть нулевой, потому что касательная в точке t2 горизонтальна. Это возможно, когда B=0.

Так как $$f(t_2)=x_2$$, то подставляя $$t=t_2$$ в (1), получаем $$f(t_2)=A+C$$. Отсюда исключаем C:

$$f(t)=x_2 + A\left[\cos\omega (t-t_2)-1\right].$$

Продифференцируем, чтобы найти скорость

$$f'(t)=-A\,\omega\sin\omega (t-t_2).$$

Нам известно положение x1 и скорость v в начальный момент времени:

$$\begin{cases} x_1\!\!\!\!\!&=x_2+A\left[\cos\omega(t_1-t_2)-1\right],\\ v\!\!\!\!\!&=-A\,\omega\sin\omega(t_1-t_2). \end{cases}$$

Из этой системы уравнений нужно найти A и $$\omega$$. Пора вводить новую переменную $$k=\omega(t_2-t_1)$$ вместо $$\omega$$. Ее смысл — разность фаз синусоиды в начальной и конечной точке. Например, для графика на рис. 4 $$k=\pi$$, потому что на промежутке $$(t_1,t_2)$$ укладывается полупериод синусоиды. На рис. 5 $$k<\pi$$, потому что $$t_2-t_1$$ меньше половины периода.

После подстановки и небольших преобразований приходим к системе

$$\begin{cases} x_2-x_1&=A\left(1-\cos k\right),\\ v(t_2-t_1)\!\!\!\!&=A\,k\sin k. \end{cases}$$

Разделим почленно первое уравнение на второе:

$${x_2-x_1\over v(t_2-t_1)}={1-\cos k\over k\sin k}\quad \Rightarrow\quad{1-\cos k\over\sin k}=\alpha k,\quad\text{где} \ \alpha={x_2-x_1\over v(t_2-t_1)}.$$

Параметр $$\alpha$$ в правой части известен заранее. Он определяет требуемый характер движения. Если $$\alpha\gg1$$, то начальная скорость мала, тело сначала должно ускориться. Если $$\alpha\ll1$$, начальная скорость велика, тело должно замедляться.

Тригонометрические функции в левой части сводятся к тангенсу половинного угла. В итоге у нас нелинейное уравнение относительно k:

$$\text{tg}\,{k\over2}=\alpha k.$$(2)

Проанализировать его решения можно на графике. Нарисуем график левой и правой части при некоторых значениях параметра $$\alpha$$:

$$\begin{tikzpicture}\small \def\aa{1.5} \def\ab{0.3} \def\ac{-0.5} \begin{axis}[legend pos=south east,mark size=1,samples=2, restrict y to domain=-8:8, width=12cm, height=250pt, xmin=-10.5, xmax=10.5, ytick={-6,-3,...,6}, xtick={-9.4247,-3.1416,...,10}, xticklabels={$-{3\pi}$,$-{\pi}$,${\pi}$,${3\pi}$}, axis x line=center, axis y line=center, xlabel=$k$] \addplot[blue!70!black,domain=-9.4247:9.4247,semithick,samples=802]{tan(deg(x/2))}; \addplot[red]{\aa*x}; \addplot[green!70!black,domain=-9.4247:9.4247]{\ab*x}; \addplot[olive,domain=-9.4247:9.4247]{\ac*x}; \addplot[mark=*] coordinates {(2.65,3.97)} node[anchor=west,inner sep=2pt]{$A$}; \addplot[mark=*] coordinates {(8.69,2.61)} node[anchor=west,inner sep=1pt,yshift=-4pt]{$B$}; \addplot[mark=*] coordinates {(4.06,-2.03)} node[anchor=west,yshift=2]{$C$}; \legend{$y=\tg k/2$,$y=\aa\,k$,$y=\ab\,k$,$y=\ac\,k$} \end{axis} \end{tikzpicture}$$

Рис. 6. Графическое решение уравнения (2)

Обсудим получившиеся решения.

  1. Рассмотрим точку A. Это решение существует при $$\alpha>1/2$$ и соответствует изображенному на рисунке 5: $$\begin{tikzpicture} \def\t{1} \def\r{3.1415} \begin{axis}[width=1.9cm,height=2cm,hide axis,ticks=none, xmin=\t,xmax=\r,mark size=0.3] \addplot[smooth,blue,domain=\t:\r,samples=80] {-cos(deg(x))}; \addplot[mark=*] coordinates {(\t,-cos(deg(x)))}; \addplot[mark=*] coordinates {(\r,-cos(deg(x)))}; \end{axis} \end{tikzpicture}$$. Как ожидалось, $$k<\pi$$. В пределе нулевой скорости $$\alpha\to\infty$$, красная прямая совпадет с осью ординат, точка A уйдет по тангенсоиде в бесконечность. В этом пределе $$k\to\pi$$. Пока всё идет правильно.

  2. Точка C отвечает значению $$\alpha<0$$. Такое случается, когда тело в первый момент времени движется вперед, а надо двигаться назад. Теперь $$\pi<k<2\pi$$. Движение описывается фрагментом синусоиды, большим чем полупериод, но меньшим, чем период: $$\begin{tikzpicture} \def\t{-1.7} \def\r{3.1415} \begin{axis}[width=2.2cm,height=2cm,hide axis,ticks=none, xmin=\t,xmax=\r,mark size=0.3] \addplot[smooth,blue,domain=\t:\r,samples=80] {cos(deg(x))}; \addplot[mark=*] coordinates {(\t,cos(deg(x)))}; \addplot[mark=*] coordinates {(\r,cos(deg(x)))}; \end{axis} \end{tikzpicture}$$. Тело тормозит, останавливается, движется назад и останавливается в требуемом месте.

  3. Из графика видно, что при $$0<\alpha<1/2$$ точка B попадает в диапазон $$2\pi<k<3\pi$$. Тело пройдет по синусоиде больше, чем полный период колебаний: $$\begin{tikzpicture} \def\t{-1.6} \def\r{2*3.1415} \begin{axis}[width=2.5cm,height=2cm,hide axis,ticks=none, xmin=\t,xmax=\r,mark size=0.3] \addplot[smooth,blue,domain=\t:\r,samples=80] {cos(deg(x))}; \addplot[mark=*] coordinates {(\t,cos(deg(x)))}; \addplot[mark=*] coordinates {(\r,cos(deg(x)))}; \end{axis} \end{tikzpicture}$$. Причина такого странного решения в том, что точка остановки находится слишком близко по сравнению с характерным расстоянием v (t2t1). Поэтому провести синусоиду без дополнительной остановки и возврата не получится.

Небольшое лирическое отступление: похожие на (2) уравнения возникают при решении задач квантовой механики про уровни энергии частиц в прямоугольных потенциальных ямах. Там приходится сшивать, например, синусоиду и экспоненту. Условие отсутствия изломов дает подобные уравнения с бесконечным количеством корней.

Приближенное решение

Мы решили математическую задачу проведения синусоиды, но жизнь от этого проще не стала. Во-первых, чтобы определить параметры синусоиды, надо решить нелинейное уравнение (2). Привет, итерационные методы! Во-вторых, уравнение имеет бесконечное количество решений, а требуемое решение не всегда существует.

Эти трудности возникли от того, что мы зафиксировали продолжительность анимации ровно в 200 миллисекунд. Однако ничего страшного не случится, если анимация продлится, скажем, 180 миллисекунд. Или даже 250 миллисекунд. Нам важнее остановка в заданном месте, а точной продолжительностью анимации мы пожертвуем для упрощения расчетов.

Ослабив требования на продолжительность анимации, мы проделаем такой трюк. Предположим, что у нас есть приближенное решение $$k'$$ нелинейного уравнения (2). Оно является точным решением уравнения с другим параметром

$$\alpha'={1\over k'}\,\text{tg}{k'\over 2},$$

Ему соответствует другое время окончания анимации:

$$t_2'=t_1+{x_2-x_1\over v\alpha'}.$$

Теперь неизвестные параметры траектории A и $$\omega$$ элементарно выражаются через $$k'$$ и $$\alpha'$$.

Я подобрал подходящее для наших целей приближение к уравнению (2):

$${1\over 2\alpha}\approx1-\left({k\over\pi}\right)^2.$$

Синяя сплошная линия соответствует точному уравнению (2), а красная пунктирная — его приближению:

$$\begin{tikzpicture}\small \begin{axis}[legend pos=south east, restrict y to domain=-8:8, width=12cm, xmin=-7.3, xmax=7.3, ytick={-6,-3,...,6}, xtick={-6.2832,-3.1416,...,10}, xticklabels={$-{2\pi}$,$-{\pi}$,$0$,${\pi}$,${2\pi}$}, axis x line=center,axis y line=center, xlabel=$k$,ylabel=$\alpha$] \addplot[smooth,samples=580,blue!70!black,domain=-7:7]{tan(deg(x/2))/x}; \addplot[smooth,samples=580,red,dashed,domain=-7:7]{0.5/(1 - (x/pi)^2)}; \legend{$(1/k)\,\text{tg}\,k/2$,$0.5/\!\left[1 - ({k/\pi})^2\right]$} \end{axis} \end{tikzpicture}$$

Рис. 7. Сравнение точного соотношения (2) и его приближения

А еще в случае $$0<\alpha<1/2$$ предлагаю взять $$\alpha'$$ чуть больше, чем 1/2, и сократить время анимации, чтобы избежать отскока и возврата.

Применение

Код и пример использования есть на демо-странице. Поводите мышью и посмотрите, как черный блок следует за оранжевым.

Описанную схему я разработал для синхронной прокрутки исходного кода и предпросмотра в своем редакторе математических текстов Upmath.

Идею и первоначальную реализацию нашел на демо-странице js-парсера markdown-it. В их варианте анимация получилась рваной и подтормаживающей. Тому есть несколько причин:

  1. Для анимации применяется линейная функция: $(...).stop(true).animate( {scrollTop: ...}, 100, 'linear' ). Вместо гладкого графика получается ломаная.
  2. Анимация через jQuery().stop().animate() тормозит по сравнению с requestAnimationFrame().
  3. Чтобы избежать падения производительности, «проглатываются» события onscroll, следующие чаще чем 50 миллисекунд. В моем варианте такой проблемы нет. Последовательные события onscroll корректируют положение точки остановки и не замедляют анимацию.

Чтобы добиться важной для продукта качественной анимации, я проработал метод вычисления на основе физических уравнений, и реализовал его через специальный браузерный метод requestAnimationFrame(). Метод хорошо работает при любой прокрутке: клавишами PageUp/PageDown, через перемещение полос прокрутки, колесико мыши, тачпад, тачскрин.

Статья впервые опубликована на хабре.

    Оставить комментарий

Формулы

5 апреля 2016 года, 21:29

Открываем 10 лучших публикаций на хабре за неделю и видим, что на четвертом месте статья «Сказ царя Салтана о потенциале лапласиана», на пятом — «Байесовская нейронная сеть — теперь апельсиновая (часть 2)». И обе с использованием моего сервиса для формул.

Неплохо, я считаю :)

См. Markdown and LaTeX online editor и описание на хабре.

    Оставить комментарий

Латех в вебе

22 января 2014 года, 12:19

Постоянные читатели помнят, что у меня есть движок сайтов S2, и он с помощью расширения s2_latex ищет в тексте страницы формулы на латехе и заменяет их на картинки. Расширение обращается к сервису codecogs.com. Этот сервис зачастую глючит, и я уже давно сделал свой, с блек-дже… качественным SVG и выравниванием по базовой линии.

Мой сервис делает из формул картинки. Вот, для примера, знакомое всем решение квадратного уравнения в SVG и PNG:

На обычных мониторах преимуществ у SVG нет. Но на ретине или при большом увеличении SVG выглядит более чем достойно. А за ретиной будущее.

В svg-файлы можно добавлять скрипты. Благодаря этому я решил проблему с базовой линией. Чтобы понять, в чем состоит эта проблема, достаточно посмотреть на любую страницу Википедии с формулами:

Символы в формулах крупнее и жирнее, чем в окружающем тексте. Они в буквальном смысле выпадают из окружающего текста. Меняем гротеск на антикву, увеличиваем кегль и добавляем выравнивание по базовой линии:

Теперь буквы в формулах аккуратно располагаются на тех же линиях, что и буквы в окружающем тексте. Чтобы заработала магия с выравниванием по базовой линии, к странице нужно подключить специальный скрипт.

У svg-формул есть существенный недостаток: многие браузеры искажают их при печати. В последних версиях IE и FF получается удовлетворительный результат. В браузерах на хромиуме лучше не распечатывать текст с формулами.

Я подготовил описание сервиса и инструкцию. Посмотреть сервис в работе можно в блоге о теоретической физике. В следующий раз я расскажу о том, как всё это работает.

    2 комментария
Поделиться
Записи