Идея с программной генерацией слайдов и рисунков презентации, отчета, лекционных заметок для студентов не нова, в частности сегодня вы можете создавать их с помощью Python (правда только через Power Point API), HTML, JSX, Julia, etc. Последние основаны на похожих идеях интегрирования декларативной разметки в роде Markdown + HTML и мы пойдем по их пути, расширяя это динамическими элементами, компонентами и привязкой к событиям. Звучит сложно, однако конечна цель состоит в противоположном.
Хочу предупредить, что данный подход вовлекает программирование в его классическом текстовом виде.
Осторожно: много картинок. Презентация ж
Введение и обзор
Из опыта работы в академической среде, выступлений на конференциях - презентация - это может быть критически важной частью любого публичного выступления. В российской школе этот вопрос часто опускался и предполагалось, что содержание важнее формы, однако мир не стоит на месте. Картинки в публикациях становятся богаче, и анимации не такое уже и редкое явление. Многие журналы уже требуют eye-catchy thumbnail к своим статьям, чтобы привлекать более широкую аудиторию. Однако не так много сил вовлечено к вопрос интерактивности в презентациях или в целом, в публикациях. Эта ветка может иметь больший потенциал, если обобщить это также на внутренние отчёты, заметки для лекций студентам, где это может помочь в восприятии.
Посмотрите, скажем, на этот пример
С другой стороны, перетаскивание объектов на слайде, форматирование шаблона - это все занимает время. Не дай Бог, вы хотите показать что-то в 3D (структуру белка, кристалла) - вам придется готовить GIF анимацию. Моя личная проблема с традиционном подходам к любым презентациям - разорвать цикл
-
подготовка данных в среде А
-
производство графиков в среде Б
-
экспорт в файл
-
форматирование слайда
-
идём к пункту 2, если что-то хочется поменять
А что насчёт компонент? Вы сделали что-то специфичное и хотели бы использовать это как шаблон, а может быть даже в цикле. Возможно, тогда декларативный подход к созданию мульти-медиа объектов - это для Вас
Декларативно декларируем разметку
Если возвращаться к корням, то TeX Beamer будет вероятно первым
documentclass{beamer}
title{Sample title}
author{Anonymous}
institute{Overleaf}
date{2021}
begin{document}
frame{titlepage}
begin{frame}
frametitle{Привет!}
This is some text in the first frame. This is some text in the first frame. This is some text in the first frame.
end{frame}
end{document}

TeX пугает многих своей сложностью, однако те, кто постигнет его могут делать удивительные вещи в пределах этой среды. Если мы за упрощение, то нужно что-то чуть менее строгое
# Голова
## Головок
Привет!
---
# Следующий головок
Привет, Хабр снова!

В этом примере ---
является разделителем слайдов. Очевидно, что в такой схеме мы отделяем стили от содержания. Так как этот фреймворк работает в веб-среде, отображение определяется таблицей CSS. Однако, если вам не нужен этот подход в презентациях, то можно смело нарушать его с помощью HTML
# Голова
## Головок
Привет, <span style="color:red"> Хабр</span>!

Это неочевидно, однако возможность использовать HTML/CSS дает нам возможность встраивать видео, аудио, целые веб-сайты или PDF с помощью тега iframe
и не только. В целом весь опыт развития веб-среды за последние 20-30 лет у вас в инструментах. Скажем, если вам нравятся Mermaid диаграммы, вы можете встроить их напрямую, однако в текущем виде потребуются знания JS и это скорее шаг назад к усложнению, чем к упрощению.
Можно было бы остановиться здесь, однако обещанное из заголовка и введения этой статьи все еще отсутствует
-
Динамика и интерактивность
-
Компоненты
В целом RevealJS это ведь только фреймворк, но не решение. Просто, чтобы загрузить картинку в папку проекта и указать к ней путь уже будет камнем преткновения.
Необычным и интересным примером может быть кухонный комбайн для декларативного приготовления видео (и презентаций как побочный продукт) Motion Canvas (aka Manim, но в плоскости JSX), и в целом React также отличный пример реализации компонентного подхода
import {makeScene2D, Txt} from '@motion-canvas/2d';
import {beginSlide, createRef, waitFor} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
const title = createRef<Txt>();
view.add(<Txt ref={title} />);
title().text('FIRST SLIDE');
yield* beginSlide('first slide');
yield* waitFor(1); // try doing some actual animations here
title().text('SECOND SLIDE');
yield* beginSlide('second slide');
yield* waitFor(1);
title().text('LAST SLIDE');
yield* beginSlide('last slide');
yield* waitFor(1);
});
Либо вот жареный суп пример несколько отдаленный от самих презентаций на MDX
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Desktop application
Notebook interface is shipped as an Electron application
<Tabs
defaultValue="Windows"
values={[
{label: 'Windows', value: 'Windows'},
{label: 'Linux', value: 'Linux'},
{label: 'Mac', value: 'Mac'},
]}>
<TabItem value="Windows">
- [Windows](https://github.com/JerryI/wolfram-js-frontend/releases/download/2.5.8/wljs-notebook-2.5.8-x64.exe)
</TabItem>
<TabItem value="Linux">
- [Linux (Deb)](https://github.com/JerryI/wolfram-js-frontend/releases/download/2.5.8/wljs-notebook-2.5.8-amd64.deb)
- [Linux (AppImage)](https://github.com/JerryI/wolfram-js-frontend/releases/download/2.5.8/wljs-notebook-2.5.8-x86_64.AppImage)
</TabItem>
<TabItem value="Mac">
- [M1](https://github.com/JerryI/wolfram-js-frontend/releases/download/2.5.8/wljs-notebook-2.5.8-arm64.dmg)
- [Intel](https://github.com/JerryI/wolfram-js-frontend/releases/download/2.5.8/wljs-notebook-2.5.8-x64.dmg)
</TabItem>
</Tabs>
It comes with a launcher, that takes care about all updates, files extension association and etc. Also see [releases](https://github.com/JerryI/wolfram-js-frontend/releases) page for more portable installation bundles (no docs).
Жесть какая - подумаете вы. Однако из JSX можно взять несколько отличных идей, а именно
-
компоненты в виде функций
-
каждый компонент как бы расширяет стандартные теги HTML (это не совсем так, но отражает суть дальнейшей идеи)
Т.е. сделали вы шапку на HTML, Markdown или как-то еще один раз где-то и затем на каждом слайде
<MakeTitle>Головок 1</MakeTitle>
Контент как ни в чем не бывало
<SomeWidget align="...center?"/>
Напоминает, как Beamer, так и JSX. Если вам все еще интересно, пройдемте к реализации.
Совмещаем несовмещаемое 
Мне как физику иногда проще показать расчеты, подрыгать слайдеры и все такое. Особенно если это касается лекций для студентов.
Очевидным выбором кажется JSX, в этом случае мы прибиваем нас гроздями к экосистеме React, что может быть не очень хорошо для Вас, если вы просто хотите сделать презентацию или лекцию для студентов на тему, скажем, магнетизма, не вникая в Vite, бандлеры и прочее. Также становится очевидно, что JSX или MDX ломает классический HTML, а не расширяет его.
Вероятно язык Javascript тоже не лучший кандидат для подготовки графиков, анализа данных, ведь есть R, Python, Julia, да даже старенький Matlab вероятно справится с этой задачей быстрее и проще. На момент написания статьи (шел 2024 год), пока ни одна среда не смогла сделать процесс построения графиков проще и доступнее, чем Wolfram Mathematica
ContourPlot[Cos[x] + Cos[y], {x, 0, 4 Pi}, {y, 0, 4 Pi}]

Или что-то динамическое, где можно менять тот или иной параметр - Manipulate. Никаких пакетов, никаких зависимостей - всё, чтобы сделать комфортным этот вариант программирования для математиков, физиков, химиков.
Фатальный недостаток
Есть проблема - это закрытая и сравнительно дорогая система $195 per year и сейчас недоступна в России. Но для расчетов в принципе нам она и не нужна, нам нужен язык, а он доступен всем и бесплатный и распространяется в виде консольного приложения Wolfram Engine. Многие ключевые функции Wolfram Mathematica мы воссоздали вместе с @KirillBelovTest в open-source реализации WLJS Notebook, о которой мы писали ранее на Хабрахабре Обзор 3, Обзор 2, Обзор 1.
Первоначальная идея, что наш Markdown и HTML наряду с нашими графиками, виджетами не должны ощущаться инородно
Figure = ContourPlot[Cos[x] + Cos[y], {x, 0, 4 Pi}, {y, 0, 4 Pi}];
# Первый слайд
Посмотрите на график
<Figure/>
А если хотим в рамочке и с задним фоном? Хм
# Первый слайд
Посмотрите на график
<div style="backround: gray; border: solid 1px red;">
<Figure/>
</div>
А может это сделать отдельным компонентом? Я думаю вы поняли идею. Нужно именно надмножество над HTML (+ Markdown) и WL

Wolfram Language XML (WLX) это синтаксическое расширение, которое мы написали для Wolfram Engine, позволяющее достичь поставленных целей в этой секции. Скачивать его не надо, он уже интегрирован в среду о которой речь пойдет дальше.
Ну хватит слов, давайте к делу!
TLDR; Как закодить слайд презентации 🛝
Среда исполнения и разработки
Все примеры я буду показывать в среде WLJS Notebook разрабатываемой автором @JerryI и @KirillBelovTest. Бинарники лежат здесь. Это не означает, что для показа вам нужно это приложение или его производная - браузера (даже без интернета) будет достаточно

Среда разделена на клиент и сервер. Клиентом может являться любой браузер, либо наше приложение на Electron. Большая часть вычислений происходит на сервере.
В блокноте есть несколько типов ячеек, тип определяется первой строкой - что-то в роде расширения. Можете думать, что это содержимое некого анонимного файла. Если ничего не указано, а идет сразу код предполагается, что это WL.
Сколько это стоит? Это все ваша проприетарщина... небось еще вендор-лок
0. Там нет зависимостей, кроме самого ядра языка и нескольких открытых Github репозиториев, которые установятся один раз. Среда разрабатывалась так, чтобы и через 20 лет (обычный срок для академической среды) ваши блокноты открывались также как и сейчас.
Первый слайд с картинками и графиком
Создаем новую ячейку типа слайд .slide
и "бросим" в окно ячейки какую-нибудь картинку
.slide
# Привет!

она должна автоматически загрузиться в директорию вашего блокнота и появиться как обычная Markdown ссылка. Теперь Shift + Enter (или кнопка play) и ваш слайд готов

Теперь наш пример с графиком. А зачем мелочиться, давайте сразу уйдем из двух измерений в третье. Создадим новую ячейку где нибудь и напишем функцию для построения поверхности функции Хэвисайда
square[{{imin_, imax_}, {jmin_, jmax_}}] :=
Table[
UnitStep[i - imin, imax - i] UnitStep[j - jmin, jmax - j]
, {i, 0, 20}, {j, 0, 20}]
Теперь запишем график в отдельную переменную
ГрафикНаш = ListPlot3D[square[{{2, 5}, {3, 7}}], Mesh -> None];
И закинем на слайд
.slide
# Привет!
<ГрафикНаш/>

А может бы привыкли с Python использовать библиотеку Plotly, не беда. Интерфейс взаимодействия сохранен в первозданном виде (как у JS версии)
ГрафикПлотлей = With[{data = square[{{2, 5}, {3, 7}}]},
Plotly[<|"type"->"surface", "z"->data|>]
]

Прежде чем идти далее, давайте ответим на несколько вопросов, которые могут возникнуть
Особенности работы с WLX
-
XML теги начинающиеся с маленькой буквы автоматически интерпретируются как HTML элементы
-
XML теги с большой буквы считаются символами Wolfram Language
-
Нельзя допускать незакрытых тегов
-
Чтобы избегать конфликтов Markdown с HTML, отступайте от Markdown выражения 1 строку (лучше сверху и снизу).
-
Документация на WLX и документация в контексте презентаций и Markdown
То как работать с тегами, которые имеют дочерние элементы будет рассказано в следующей секции.
Как сделать фуллскрин, показ?
-
Нажать клавишу
f
на слайде -
Использовать параметр ячейки справа Project to a New Window
Как показать все слайды?
В целом разделяя слайды с помощью ---
можно писать все в пределах одной ячейки, однако это делает структуру сложной в дальнейшем. Лучшее правило 1,2 слайда на ячейку. Затем вы можете "собрать" все слайды создав новую ячейку и назвав ее
.slides
Спасибо за внимание
Не важно, где она находится, она "соберет" контент всех других ячеек, где слово слайд в единственном числе и покажет как презентацию.
Подождите, вам не кажется, что это скорее шаг назад? А где стили, а если квадрат нарисовать и теги какие-то...
Less is more, верно ведь говорят? Что может быть менее отвлекающим, чем 1 картинка на слайд и список из трех пунктов? Если шрифт (глобальный) и другие вещи для вас все-таки важны, CSS классы доступны для каждого элемента Markdown. К примеру, если создать ячейку типа HTML или WLX, а затем выполнить
.wlx
<style>
.reveal h1 {
font-family: consolas;
}
</style>

Разумеется можно делать это по-другому и шапка с логотипом, какая-то надпись внизу, квадратики это все мы разберем.
У моего коллеги нет WLJS Notebook, как я буду показывать слайды?
Можно экспортировать в один HTML файл, можно в PDF, включая все картинки и даже интерактивность. Мы разберем это позже.
В чём смысл делать это в блокноте?
Вы собираете в одном месте данные, методику их обработки, алгоритмы etc, а также отчёт о них в виде презентации. Ваши читатели знают как вы их получили и могут повторить. Для просмотра им не нужно ничего кроме браузера. Нет внешних зависимостей (или есть, зависит от вашего выбора). Блокнот это песочница, динамическая и интерактивная.
Локальные компоненты
Колонки
Хотите сделать две колонки (на самом деле уже есть встроенная функция, но вы все равно хотите сделать свою)? Давайте объявим соотвествующий символ
.wlx
Columns[data__, OptionsPattern[]] := With[{
Style = OptionValue["Style"]
},
With[{DataList = Table[
<div>
<Item/>
</div>
, {Item, List[data]}]},
<div class="flex flex-row justify-between" style="{Style}">
<DataList/>
</div>
]
]
Options[Columns] = {"Style" -> ""};
А теперь попробуем использовать его в качестве шаблона
.slide
# Привет!
<Columns>
<p>Колонка 1</p>
<p>Колонка 2</p>
</Columns>

Будьте осторожны с аргументами, если просто написать две строчки без тега символ Columns
не поймет, что это разные колонки. Здесь p
играет роль такого разделителя. Если добавите достаточно переводов строк, можно безопасно писать на Markdown
.slide
# Привет!
<Columns>
<p>
# Головок 1
</p>
<p>
# Головок 2
</p>
</Columns>

Помните мы записали опции к Columns
? Давайте стилизуем
.slide
# Привет!
<Columns Style={"
border-radius: 4px;
color: #ffffff;
background: rgb(49 87 170);
padding: 1rem;
"}>
<p>
# Головок 1
</p>
<p>
# Головок 2
</p>
</Columns>

Разумеется символ Row
, который используется для организации ряда в обычных ячеек имеет представление и в контексте слайдов. Давайте на примере наших графиков - рассчитаем и построим Фурье-образ
square[{{imin_, imax_}, {jmin_, jmax_}}] :=
Table[
UnitStep[i - imin, imax - i] UnitStep[j - jmin, jmax - j]
, {i, 0, 20}, {j, 0, 20}]
ГрафикНаш = Row[{
ListPlot3D[square[{{2, 5}, {3, 7}}], Mesh -> None]
,
ListPlot3D[Abs@Fourier@square[{{2, 5}, {3, 7}}], Mesh -> None, ColorFunction -> "Rainbow"]
}];
и вставим его на слайд
.slide
# Пример
<ГрафикНаш/>

Разумеется такой вариант тоже сработает
{ГрафикНаш1, ГрафикНаш2} = {
ListPlot3D[square[{{2, 5}, {3, 7}}], Mesh -> None]
,
ListPlot3D[Abs@Fourier@square[{{2, 5}, {3, 7}}], Mesh -> None, ColorFunction -> "Rainbow"]
};
.slide
# Пример
<Row>
<ГрафикНаш1/>
<ГрафикНаш2/>
</Row>
Для удобства в Row
в представлении слайдов опускается List
, который обязателен в стандартной форме.
Колонтитулы
Подобным же образом можно организовать переиспользуемые колонтитулы. Вот пример с одной из конференций на которых автор показывал свои слайды
.wlx
MakeTitle[Title__String] := MakeTitle[StringJoin[Title]]
MakeTitle[Title_String] :=With[{},
<div class="relative flex w-full text-left flex-row gap-x-4" style="align-items: center; margin-bottom:1.5rem;">
<div style="bottom:0; z-index:1; position: absolute; background: linear-gradient(to left, red, blue, green); width: 100%; height: 0.7rem;"></div>
<img style="margin:0; z-index:2; padding:0rem; border-radius:100px;" width="120" src="https://www.trr360.de/wp-content/uploads/2022/04/cropped-logo_small-1.png"/>
<h2><Title/></h2>
</div>
]
Footer = With[{},
<div class="w-full ml-auto mr-auto bottom-0 text-sm absolute">
DFG Retreat Meeting TRR360: <i>C4 Ultrastrong matter-magnon coupling
</i>, Kirill Vasin
</div>
];
И так на каждом слайде можно сделать что-то в духе
.slide
<!-- .slide: style="height:100vh" -->
<MakeTitle>Ultrastrong coupling</MakeTitle>
Content goes and goes...
Content goes and goes...
Content goes and goes...
<Footer/>

В примере выше видно некие забавные комментарии в HTML, это стилизация конкретно этого слайда выполненная с помощью механизма, который предлагает RevealJS. Мы к этому еще вернёмся.
Стилизация (inline)
Автор RevealJS в принципе предлагает писать классы и стили в Markdown коде как
.slide
<!-- .slide: data-background-color="black" -->
# Красное <!-- .element: style="color:red" -->
# Белое <!-- .element: style="color:white" -->

Вместо, скажем глобальных, что тоже сработает. Например хотим поменять везде шрифт
.wlx
<style>
.reveal {
font-family: Arial;
}
</style>
Либо придумаем свой стиль
.wlx
<style>
.highlightClass {
background: yellow;
}
</style>
И тогда есть два варианта его применить
.slide
<!-- .slide: data-background-color="black" -->
Стиль <!-- .element: class="highlightClass" -->

либо так
.slide
<!-- .slide: data-background-color="black" -->
<span class="highlightClass">Стиль</span>

Наиболее частный вопрос: как выровнять по левому краю?
.slide
<!-- .slide: style="text-align:left" -->
# Heading
Some text

Фрагменты
В RevealJS вводится такое понятие как фрагменты для анимации элементов. Анимаций много, их можно посмотреть здесь. Мы же ограничимся "появлением", делается это с помощью техники стилизации. Вернемся к алкогольному примеру
.slide
<!-- .slide: data-background-color="black" -->
# Красное <!-- .element: style="color:red" class="fragment" -->
# Белое <!-- .element: style="color:white" class="fragment" -->
Назначается анимация с помощью класса fragment
и каждый шаг анимации двигается при нажатии на стрелку вправо

Чтобы повлиять на порядок следования используется специальный атрибут
.slide
<!-- .slide: data-background-color="black" -->
# Красное <!-- .element: style="color:red" data-fragment-index="1" class="fragment" -->
# Белое <!-- .element: style="color:white" data-fragment-index="1" class="fragment" -->

Ну и очевидно, вы можете писать эти классы точно также для обычных элементов как в HTML.
Кроме того, на эти события можно подписываться со стороны WL, но об этом чуть позже.
Уравнения и диаграммы, код
Без LaTeX в формулах никуда, да и зачем изобретать велосипед. Одни поддерживаются из коробки с одной оговоркой: желательно избегать одиночных обратных косых черт
.slide
## LaTeX
$$
\begin{align*}
\mathbf{E}(t,x) &= \sum_{\omega} \mathbf{E}_0^{\omega} ~ exp\Big( i\omega t - \frac{i\hat{n}(\omega) \omega x}{c}\Big) \\ &= \sum\mathbf{E}_0^{\omega} \colorbox{white}{$exp(-\frac{\alpha x}{2})$} ~exp\Big(i\omega t - \frac{i n \omega x}{c}\Big)
\end{align*}
$$

А можно анимировать? Почти как в Manim
## LaTeX
$$
\begin{align*}
\mathbf{E}(t,x) &= \sum_{\omega} \mathbf{E}_0^{\omega} ~ exp\Big( i\omega t - \frac{i\hat{n}(\omega) \omega x}{c}\Big) \\ &= \sum\mathbf{E}_0^{\omega} \colorbox{white}{$exp(-\frac{\alpha x}{2})$} ~exp\Big(i\omega t - \frac{i n \omega x}{c}\Big)
\end{align*}
$$ <!-- .element: data-eq-speed="0.1" -->
С фрагментами оно будет работать предсказуемым образом - анимация по появлению

В одном из стандартных расширений WLJS Notebook есть такой тип ячеек Mermaid, описывающий диаграммы. В целом, это возможно - отображать ячейку внутри ячейки или на любом другом холсте с помощью компонента CellView
MyDiagram = CellView["
graph LR
A[Text Header 3200 byte] --> B[Binary Header 400 byte]
B --> C1[240 byte 1-st trace header] --> T1[samples of 1-st trace]
B --> C2[240 byte 2-st trace header] --> T2[samples of 1-st trace]
B --> CN[240 byte n-st trace header] --> T3[samples of 1-st trace]
", ImageSize->650, "Display"->"mermaid"]
Теперь вставим ее на слайд точно также, как и любой другой объект
.slide
# Чужеродный объект
<MyDiagram/>

А что насчет исходного кода? В целом если речь касается Wolfram Language, то это можно решить с помощью собственного компонента и EditorView
.wlx
CodeInset[str_String] := With[{Fe = EditorView[str]},
<div style="text-align: left; font-size:14px;"><Fe/></div>
]
<style>
.slide-frontend-object .cm-editor {
text-align: left;
}
</style>
.slide
## Исходный код на слайде
<CodeInset>
(*SbB[*)Subscript[B(*|*),(*|*)k_, q_](*]SbB*)[coords_] := Sum[ With[{[Theta] = ToSphericalCoordinates[c][[2]], [Phi] = ToSphericalCoordinates[c][[3]]},
(*SpB[*)Power[(-1)(*|*),(*|*)q](*]SpB*) (*SbB[*)Subscript[a(*|*),(*|*)k](*]SbB*)[Norm[c]] (*SqB[*)Sqrt[(*FB[*)((4Pi)(*,*)/(*,*)(2k + 1))(*]FB*)](*]SqB*) SphericalHarmonicY[k,-q, [Theta], [Phi]]
]
, {c, coords}]
</CodeInset>
Не пугайтесь этих странных комментариев, это автоматически сгенерированная структура для синтаксического сахара WL. Ее не нужно писать вручную, а просто скопировать текст из обычный ячейки.

В целом, это - песочница. Вы можете переопределить любые классы и стили.
Excalidraw
Это известный во времена коронавирусов векторный редактор-доска. Невероятно простая и удобная вещь в обращении, производящая SVG графику. Чтобы встроить его, мы решили расширить синтаксис Markdown и сделать его в виде синтаксического сахара
.slide
!![]
Вот такая комбинация сотворит окно, где можно рисовать любую графику. Исходное состояние сохраняется за виджетом, что позволяет встраивать его внутрь других тегов

Не все работает идеально, его поддержка добавлена относительно недавно.
"Вы сказали про динамику в заголовке..." 
Ранее я упомянул про фрагменты и события, так вот, к ним можно привязываться. Начнем с чего-то простого - счетчик, который показывает некую статистику
.wlx
Stat[Text_, OptionsPattern[]] := With[{
Count = OptionValue["Count"]
},
<div class="text-center text-gray-600 m-4 p-4 rounded bg-gray-100 flex flex-col">
<Count/>
<span class="text-md"><Text/></span>
</div>
]
Options[Stat] = {"Count"->1};
Эта функция принимает один аргумент и также опции. Для стилизации доступен Tailwind, но не весь. Если поставить это на слайд
.slide
# Простой счетчик
<Stat Count={11}>Число публикаций</Stat>
получим статический объект

Для того, чтобы добавить динамики, мы можем использовать HTMLView
для динамической подмены цифр в нашем табло (он просто обновляет .innerHTML
). Кроме того, обернем все в модуль, чтобы локально хранить все переменные
.wlx
Stat[Text_, OptionsPattern[]] := Module[{
cnt = 0, (* число *)
task
}, With[{
ev = CreateUUID[],
HTMLCounter = HTMLView[cnt // Offload], (* счетчик *)
max = OptionValue["Count"]
},
EventHandler[ev, { (* подписываемся на события слайда *)
"Destroy" -> Function[Null, (* удалили ячейку *)
EventRemove[ev];
If[task["TaskStatus"] === "Running", TaskRemove[task]];
ClearAll[task];
],
"Left" -> Function[Null, (* покинули слайд *)
cnt = 0;
],
"Slide" -> Function[Null, (* перешли на слайд *)
If[task["TaskStatus"] === "Running", TaskRemove[task]];
task = SetInterval[
If[cnt < max, cnt = cnt + 1,
TaskRemove[task];
];
, 15];
]
}];
<div class="text-center text-gray-600 m-4 p-4 rounded bg-gray-100 flex flex-col">
<HTMLCounter/>
<span class="text-md"><Text/></span>
<SlideEventListener Id={ev}/>
</div>
] ]
Options[Stat] = {"Count"->1};
Здесь происходят интересные вещи. Каждый слайд в принципе генерирует разного рода события: перешли на него, фрагмент появился, закрыли слайд, удалили ячейку с презентацией. На это все можно подписываться с помощью символа SlideEventListener
который их перехватывает и направляет по сгенерированному Id. В целом архитектуру виджета можно проиллюстрировать следующим образом

Разумеется дополнительно приходится обрабатывать случаи если презентацию закрыли, либо ушли со слайда. Каждый такой компонент лексически изолирован и не должен влиять на что-либо вне его. На каждый новый экземпляр будет сгенерированы новые переменные. Давайте поставим их несколько!
.slide
# Простой счетчик
<Row>
<Stat Count={11}>Число публикаций</Stat>
<Stat Count={110}>Количество часов</Stat>
<Stat Count={1010}>Количество символов</Stat>
</Row>
В результате получим следующее

Разумеется никто не запрещает внедрять свои Javascript скрипты, связывать их с функциями на ядре WL и делать что-то сложное, но это тема для отдельной статьи и не так часто требуется на практике.
Интерактивность
Помните про Manipulate
? У нас есть его более быстрая версия. Попробуем сначала в обычной ячейке
Widget = ManipulatePlot[{
Sin[x t],
Sum[Sin[w x t]/w, {w, 1, n}]
}, {t,0,10Pi}, {x, 0, 2}, {n, 1, 15, 1}]

А теперь давайте попробуем ее на слайде
.slide
# Интерактивность
Можно подрыгать слайдеры
<Widget/>

Важно отметить, что это не заранее просчитанные положения ползунков. Каждое движение такого слайдера вызывает обработчик и ядро пересчитывает заново все кривые. Однако это не означает, что вы не сможете выложить эту куда-нибудь себе на сайт - попробуйте сами! Чуть позже дойдем и до этой части.
Предрасчитанные анимации
Есть также готовый вариант с анимацией, его каждая кривая будет заранее рассчитана и все кадры запакованы в массив, который будет воспроизводиться бесконечно. Интерфейс точно такой же, как и у ManipulatePlot
AnimatePlot[Sum[(Sin[2π(2j - 1) x])/(2j), {j, 1.0, n}], {x, -1, 1}, {n, 1, 30, 1}]

Реактивность
Очевидно, что все, что было показано
ManipulatePlot
иAnimatePlot
это не специальные символы для системы с точки зрения архитектуры, а комбинация мелких кирпичиков, о которой мы поговорим далее.
Мы также можем привязываться к фрагментам, чтобы при нажатии на кнопку далее появился текст и сменилось наполнение графика. Есть одна проблема с этим - вам нужно самостоятельно собрать ваш график из примитивов, чтобы воспользоваться такой динамикой. Что ж попробуем сначала в обычной ячейке
myData = Table[{x, Sin[x]}, {x,0,5Pi,0.1}];
Graphics[{
ColorData[97][1], Line[myData // Offload]
}, Axes->True, TransitionDuration->1000]

Здесь есть важный символ Offload
внутри примитива Line
. Он не позволяет символу myData
раскрыться в обычный массив и на клиент приходит просто указатель. Благодаря этому указателю примитив Line
подписывается на обновления символа myData
. Не все функции поддерживают такую реактивность, все указано в шапке каждого такого примитива на сайте с документацией.
Теперь если обновим наш символ выполнив где-нибудь
myData = Table[{x, Sinc[x]}, {x,0,5Pi,0.1}];

Чтобы поместить все это на слайд вы можете оставить все как есть с глобальными переменными, либо опять свернуть в модули, чтобы не мусорить в скоп. И давайте добавим летающий диск, просто так
.wlx
PlotWidget[OptionsPattern[]] := Module[{
data = OptionValue["DataA"],
disk = OptionValue["DataA"] // Last
},
With[{
Canvas = Graphics[{
ColorData[97][1], Line[data // Offload],
ColorData[97][3], Disk[disk // Offload, 0.5]
}, Axes->True, ImageSize->500, PlotRange->{{-0.2, 1.1 5Pi}, 1.1{-1,1}},
TransitionDuration->500],
uid = CreateUUID[],
dataA = OptionValue["DataA"],
dataB = OptionValue["DataB"]
},
EventHandler[uid, {
"fragment-1" -> Function[Null,
data = dataB;
disk = dataB // Last;
],
("Left" | "Destroy" | "Slide") -> Function[Null,
data = dataA;
disk = dataB // First;
]
}];
<div class="flex flex-col gap-y-2">
<Canvas/>
<div class="fragment">Dummy text</div>
<SlideEventListener Id={uid}/>
</div>
]
]
Options[PlotWidget] = {"DataA"->{}, "DataB"->{}};
Как говорилось выше, фрагменты тоже генерируют события. Здесь неявно полагается, что это будет единственный фрагмент на странице, т.е. его индекс неявно равен 1, поэтому обработчик подписывается на fragment-1
. Теперь можно сгенерировать наши данные
{dataA, dataB} = {
Table[{x, Sin[x]}, {x,0,5Pi,0.1}],
Table[{x, Tan[x]}, {x,0,5Pi,0.1}]
};
и затем "подключить" их к нашему виджету на слайде
.slide
# Реактивность
<PlotWidget DataA={dataA} DataB={dataB}/>
---
Идем назад?

Ничего не мешает привязываться к событиям извне. Я покажу это в виде псевдо-кода, но суть должна быть ясна
.slide
Первый фрагмент <!-- .element: data-fragment-index="1" class="fragment" -->
Второй фрагмент <!-- .element: data-fragment-index="2" class="fragment" -->
<!-- Какой-то виджет -->
<SomeWidget1 Event={"my-first-slide"}/>
<!-- Какой-то виджет -->
<SomeWidget2 Event={"my-first-slide"}/>
<SlideEventListener Id={"my-first-slide"}/>
Т.е. каждый виджет подписывается на один и тот же глобальный Id
события, а дальше они сами разбираются, кто и когда и на что реагирует. Вы как бы соединяете проводами извне различные модули.
Можно рассмотреть и более сложный пример, где анимируется уравнение и подгонка параметров кривой вида
Само уравнение можно записывать в виде кода на языке WL используя EditorView
, который поддерживает реактивность. Важно, чтобы входные данные были строкой
.wlx
FittingWidget := Module[{
buffer = {},
Omega = 7.,
text = "",
recalc,
target,
trigger = 0,
ev = CreateUUID[],
id = CreateUUID[],
blocked = True,
p = 0.01,
EditorPart1,
EditorPart2,
CanvasPart
},
EventHandler[id, {
("Left" | "Destroy") -> Function[Null,
blocked = True;
],
"Slide" -> Function[Null, (* задерживаем анимацию *)
SetTimeout[
blocked = False;
EventFire[ev, True];
, 500];
]
}];
recalc[p_] := (
text = StringJoin["(*SbB[*)Subscript[ω(*|*),(*|*)0](*]SbB*) = ", Round[p Omega, 0.01] // ToString, "(*SpB[*)Power[s(*|*),(*|*)-1/2](*]SpB*)"];
buffer = {#, Sin[p Omega Sqrt[#]]} &/@ Range[0., 25., 0.1];
);
target = {#, Sin[Omega Sqrt[#]]} &/@ Range[0., 25., 0.1];
recalc[0.01];
EventHandler[ev, Function[Null,
If[blocked, Return[]];
trigger += 1;
If[Mod[trigger, 2] == 0,
recalc[p];
p = p + 0.05 (1.0033 - p);
If[Abs[p - 1.0] < Power[10,-3], blocked = True; Print["Stopped"]];
];
]];
CanvasPart = Graphics[{
Blue, Line[target], Red, Line[buffer // Offload],
AnimationFrameListener[trigger // Offload, "Event"->ev]
}, Axes->True, Frame->True, PlotRange->{{0,25}, {-1,1}}];
EditorPart1 = EditorView["y(t) = sin((*SbB[*)Subscript[ω(*|*),(*|*)0](*]SbB*)(*SqB[*)Sqrt[t](*]SqB*)) "] ;
EditorPart2 = EditorView[text // Offload] ;
<div class="flex flex-row" >
<div class="flex flex-col text-left" style="padding: 2rem 0">
<EditorPart1/>
<EditorPart2/>
</div>
<CanvasPart/>
<div class="fragment"></div>
<SlideEventListener Id={id}/>
</div>
]
Можно опять встретить здесь эти "забавные комментарии", однако их никогда не нужно вводить вручную. Если открыть обычную ячейку и вставить их туда, можно увидеть, что это обычное выражение с сахарной посыпкой, пусть и некорректное с точки зрения WL

И все эти конструкции можно вводить либо с помощью комбинации клавиш (Ctrl+/, Ctrl+2 и т.д.), либо с помощью тулбокса Special characters из палитры команд.
Ключевая идея анимации в коде сверху это AnimationFrameListener
, что весьма узнаваемое в среде веб-разработчиков. Он привязан к циклу обновления кадров браузера и отправляет событие каждый раз перед этим и затем блокируется до тех пор, пока переменная в первом аргументе trigger
не обновится и затем цикл повторяется. Это позволяет не перегружать процессор на слабых машинах, либо если возникла задержка в расчетах. Остальное это минимальная обвязка для запуска и остановки анимации. Перейдем к слайду
.slide
# Подгонка
<FittingWidget/>

Процедурные фоны
Можно продолжать упражнения с WL в том же духе и сделать процедурный фон для слайдов. Скажем, несколько шариков, двигающихся по орбитам
BackImageDynamic := Graphics[{
Black, Rectangle[{0,0}, {1,1}], Red,
curveDynamicGenerator[{0.5,0.5}, 0.8], Blue,
curveDynamicGenerator[{0.5,0.5}, 0.3]
}, Controls->False, ImagePadding->0, TransitionDuration->400, ImageSize->{960,700}, PlotRange->{{0,1}, {0,1}}];
где curveDynamicGenerator
возвращает динамический объект
curveDynamicGenerator[center_, radius_] := With[{},
Module[{
pts = Table[Norm[center - radius] {Sin[i], Cos[i]} +
center, {i, 0, 2 Pi + 0.1, 0.1}],
disk = {10,10},
ev = CreateUUID[],
modulation = 0.,
phase = 0.,
trigger = 1,
initial = 12. RandomInteger[{0,10}]
},
EventHandler[ev, Function[Null,
If[Mod[trigger, 5] != 0, trigger = trigger + 1; Return[]];
pts = Table[(
Norm[center - radius]
+ 0.02 modulation Sin[50. i + 30 phase]
) {Sin[i], Cos[i]} + center
, {i, 0, 2 Pi + 0.1, 0.01}];
disk = With[{i = 3. phase + initial},
(Norm[center - radius]
+ 0.01 modulation Sin[50. i + 30 phase]
) {Sin[i], Cos[i]} + center
];
phase = phase + 0.02;
modulation = Sin[phase/2];
trigger = trigger + 1;
]];
{
Line[pts // Offload],
Disk[disk // Offload, 0.013],
AnimationFrameListener[trigger // Offload, "Event"->ev]
}
]]
Здесь мы искусственно замедляем ответы ядра с помощью переменной trigger в 5 раз, чтобы наш фон не отнимал много ресурсов. Интерполяция на стороне браузера с помощью TransitionDuration->400
обычно чуть "дешевле". В целом это проще и безопаснее, чем SetInterval
, о котором надо позаботиться и в какой-то момент остановить.
Расположим наш фон на слайде и убедимся, что он не занимает там места с помощью position
свойства CSS
.slide
<!-- .element: data-background-color="black" -->
<!-- .slide: style="height:100vh; color: white;" -->
<div class="flex flex-col h-full"> <!-- классическая проблема центрирования div -->
<div class="absolute w-full h-full" style="scale: 1.1; left:-30px; z-index:-100">
<BackImageDynamic/>
</div>
<div class="mt-auto mb-auto">
# Динамический фон
Анимируется до тех пор, пока слайд виден
</div>
</div>

Накинув свойство CSS filter
можно расфокусировать фон, если он отвлекает читателя.
Интегрируем внешние библиотеки
Загрузим библиотеку конфетти
.wlx
<script src="https://cdn.jsdelivr.net/npm/party-js@latest/bundle/party.min.js"></script>
Теперь сделаем функцию на Javascript, которая будет запускать конфетти
.js
core.RunFireworks = async (args, env) => {
const id = await interpretate(args[0], env);
party.confetti(document.getElementById(id).parentNode, {
count: party.variation.range(20, 40),
size: party.variation.range(0.8, 2.2),
});
}
Она принимает 1 аргумент - Id элемента и запускает кофетти на его родителе. Подробности того, как писать такие функции достойно отдельного сообщения. Сегодня не об этом.
В целом, таким образом реализованы большинство стандартных функций. Если Вам не хватает инструментов - вы просто делаете их сами и переносите из блокнота в блокнот. Мы не поменяем API и через 10 лет.
А теперь сделаем пустой элемент, который будет привязываться к событиям на слайде
.wlx
Party := With[{UId = CreateUUID[], Ev = CreateUUID[], win = CurrentWindow[]},
EventHandler[Ev, {
"Slide" -> Function[Null,
FrontSubmit[RunFireworks[UId], "Window"->win]
]
}];
<div id="{UId}">
<SlideEventListener Id={Ev}/>
</div>
]
Где создастся пустой DIV элемент, внутри которого находится шпион, который передает ядру все события. Как только мелькает паттерн "Slide"
, оно выполнит на стороне JS нашу функцию. Попробуем расположить это на слайде
.slide
# Сейчас запустим
---
# Конфетти!
<Party/>

Как видите, можно создавать слайды программно очень просто, но при большом желании есть возможность сделать противоположное и превратить это в CodeSandbox.
Экспортируем в одиночный HTML файл 
Так как все графические элементы, UI и ячейки блокнота отрисовываются браузером, не стоит особого труда отправить все это в HTML файл. Разумеется у Вас есть выбор: зависеть от интернета (CDN) или нет. Работая в университете на древних машинах мой выбор обычно на втором, в любом случае это можно назначить в настройках, как и выбрать, что исключить из бандла. Если бы не используете 3D графику на слайдах, то зачем этим библиотекам занимать место.
Прежде чем запаковать все это, я напомню: для того, чтобы собрать слайды из разных ячеек в единую презентацию нужно создать новую ячейку где-угодно (но для зрителей лучше где-то в начале)
.slides
Спасибо за внимание
и исполнить. Итак, закончив со слайдами, нажимаем на иконку share

В результате мы получим одиночный файл

Можете посмотреть сами по ссылке
В таким варианте вам будут доступны
-
Все предрасчитанные объекты типа
AnimatePlot
-
Все внешние картинки будут сконвертированы в base64 и внедрены внутрь
-
3D/2D графика, Plotly и прочие графические объекты будут работать
-
Данные сохраненные внутри блокнота, исходный код и сырые данные. HTML файл все еще можно сконвертировать обратно в блокнот.
Однако недоступно
-
Слайдеры, динамика/реактивность
Чтобы решить эту проблему есть еще один вариант

Он делиться на несколько этапов - первый это нюхач (sniffer)
Нюхач
Нюхач внедряется между ядром и клиентом и начинает перехватывать и записывать все события в общем виде и ответы сервера на изменение реактивных символов. В этом смысле не так важно, используете ли вы стандартные элементы ввода (слайдеры) или свои собственные написанные на JS/HTML

На этом этапе Вам нужно подвигать все возможные слайдеры в презентации в полном диапазоне. Довольно примитивный статистический автоматически выявит все связи и кросс-зависимости, а также запишет все возможные состояния элементов и ответы на них. Попробуем на примере
Виджет := ManipulatePlot[Sum[(Sin[2π(2j - 1) x])/(2j), {j, 1.0, n}], {x, -1, 1}, {n, 1, 30, 1}];
.slide
# Пример оффлайн интерактивности
Наш виджет работает и без WL
<Виджет/>

Как только все готово, нажимаем продолжить и переходим к следующему этапу.
Сэмплер
Этот этап полностью автоматический, система сама пройдется по всем событиям и запишет ответы сервера используя все комбинации событий, если они кореллируют. После этого данные будут сжаты и сохранены по пути, который вы укажите.
Как это работает в формате HTML?
После того, как мы отделились от блокнота в самостоятельную сущность, все интерактивные объекты в целом-то и не в курсе того, что ядра больше нет и соединения с сервером тоже нет. Они продолжают жить своей жизнью и вызывать функции API, где сейчас сидит наш шпион с большой хэш-таблицей и алгоритмом, как отвечать на звонки клиентов. Это очень напоминает колл-центр

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

Пример также доступен по ссылке
Разумеется не все работает идеально, могут быть проблемы, если вы используете чекбоксы или окно ввода текста. Просим в Github Issues по любым случаям на практике.
Опубликованные примеры 
Подобным экспортом было создано все собрание примеров на странице WLJS Demonstration Project. Здесь же приведены опубликованные примеры именно с презентациями, которые автор использовал в преподавании и в отчетах
-
Простенькая презентация How to animate things
-
THz study of Fe2Mo3O8 in magnetic field report. Augsburg 2023
-
Пример из этой статьи. ManipulatePlot
-
Пример из этой статьи. Два слайда с анимацией
Есть и другие, однако публикация их требует явного согласия соавторов, что бывает иногда проблематично :)
Заключение
Очевидно, что изначальная идея от сложного к простому лишь достигается частично. Многие вещи, если не задумываться об анимации и стилизации выглядят проще, чем в PowerPoint благодаря Markdown, но с другой стороны открывает простор для творчества. Это немного больше, чем слайды с презентациями - это фактически фабрика для их генерации, где описаны методы создания каждой иллюстрации, данные из которых они собираются и то, как они обрабатываются. В целом, если настроить ваш набор инструментов один раз, вам не составит большого труда просто менять текст и графическое содержание и не думать о расположении элементов.
Динамика или же интерактивность (оффлайн) уже открывает совершенно новую область, где пока очень мало таких примеров. Из своего опыта: возможность коллеге или студенту покрутить крутилки на ваших слайдах и увидеть результат значительно улучшает восприятие материала. В этом вас не ограничит бесплатная лицензия Wolfram, так как конечный формат может быть сгенерированным HTML файлом с Javascript кодом внутри.
Ссылки
-
RevealJS Markdown презентации (технология)
-
Excalidraw доска для рисования (технология)
-
WLJS Notebook документация
-
Wolfram Engine интерпретатор языка WL (технология)
-
Контакты разработчиков проекта @JerryI и @KirillBelovTest
Автор: JerryI