Презентация приложения для проведения презентаций

Здравствуйте, меня зовут Дмитрий Карловский и я иногда выступаю на конференциях, митапах, а так же с недавних пор сам вхожу в команду организаторов одного из них — PiterJS. Недавно у нас был юбилей — 40 проведённых митапов. Но вместо того, чтобы расслабиться и получать поздравления, мы запарились и сами подготовили доклады от организаторов.

Тестируем голосовое управление

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

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

А пока, предлагаю вам рассказ про веб приложение для проведения презентаций $hyoo_slides, которое я использую для всех своих выступлений. Видеозапись доступна на YouTube, но там не всё. Можете читать этот рассказ как статью, так и открыть в интерфейсе самого приложения. Далее я расскажу вам, сколько всего оно умеет, и как работает.

Презентация в режиме слушателя

Перед вами интерфейс, который видят слушатели во время выступления. В нём нет ничего лишнего.

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

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

Основная идея в том, чтобы позволить докладчику сконцентрироваться на содержимом и не беспокоиться об оформлении. Докладу вовсе не нужны какие-то особые красоты и эффектные анимации. Иначе оформление будет перетягивать внимание на себя. А содержание рискует пролететь мимо ушей.

Потому, что я - Бэтмен!

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

Интерфейс докладчика разделён на две части.

Презентация в режиме докладчика

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

Поэтому в нашем случае контент пишется как статья в формате MarkDown и выкладывается на какой-нибудь GitHub Pages. А всё остальное берёт на себя веб-приложение.

# Название первого слайда

Содержимое первого слайда

# Название второго слайда

Содержимое второго слайда

После выступления текстовую расшифровку доклада часто выкладывают в виде статьи на каком-нибудь Хабре. Делать её могут месяц, два, пол года. Задача это неблагодарная переводить речь в текст. Но, благодаря тому, что исходники слайдов уже в формате markdown и содержат комментарии докладчика, их в таком виде можно сразу публиковать.

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

- *Акцент*
- **Сильный акцент**
- ~~Удаление~~
- ```Код```


  • Акцент
  • Сильный акцент
  • Удаление
  • Код

Разумеется доступны и различные средства инлайн форматирования.

Блоки исходного кода, разумеется, тоже поддерживаются, и раскрашиваются во все цвета радуги. Прям как вы любите.

```javascript
const hello = ()=>

    Hello, "world"!












```
const hello = ()=>

    Hello, "world"!

Сравнивать различные штуки вам помогут таблицы. Например, давайте посмотрим, чем $hyoo_slides лучше ближайших конкурентов — shwr.me и google slides

|                          | shwr.me | google slides | slides.hyoo.ru |
|--------------------------|---------|---------------|----------------|
| Исходник в MarkDown      | -       | -             | +              |
| Двухпанельный режим      | -       | +             | +              |
| Автопереключение слайдов | -       | -             | +              |
| Оффлайн                  | -       | +             | +              |

Как видно, $hyoo_slides бьёт конкурентов по всем фронтам. Кроме тех, которые не вошли в таблицу, конечно же.

Ну да лано, таблицы — это скучно.

Держите котейку.

![Котейка](https://github.com/nin-jin/slides/raw/master/slides/cat.gif)

Котейка

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

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

![Нецелевая аудитория](https://www.youtube.com/embed/exfBX2pb7AQ?autoplay=1)

Нецелевая аудитория

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

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

Стрелочки

Но что если кликер сломался?

Вы, наверно, думаете, что в зале у меня тут где-то есть засланный казачок, который переключает слайды вместо меня? Однако, это не так.


  • Дальше, пожалуйста.
  • Назад, пожалуйста.
  • Слайд номер 5, будь добра.
  • На начало, пожалуйста.
  • В конец, пожалуйста.
  • Найди «котейку», будь добра.
  • Повтори, пожалуйста.
  • Помолчи, будь любезна.
  • Продолжай, пожалуйста.
  • Выключи свет, будь любезна.

Повтори, пожалуйста. Голос свыше повторяет последнюю фразу.

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

Но повторять эти кодовые фразы по десять раз подряд — это скучно, поэтому $hyoo_slides умеет анализировать заметки докладчика и, когда вы произносите последнее слово, переключать слайд автоматически.

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

Жесты

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

Но тут он заезжает в тоннель и у него пропадает связь.

Нет сети

Не беда, у нас же Web2.0 HTML5 Progressive Web Application с полной работоспособностью даже, когда нет интернета.

Но тут к вам подходят организаторы и говорят: «Хотим PDF».

Отпечаток

Какой PDF? У нас же тут мультимедийный интерактивный Web2.0 HTML5 Progressive Web Application. Однако, вам объясняют, что приложение сегодня есть, а завтра его уже нет. А если есть, то хочет денежку. А если не хочет, то слайды там уже могут быть изменены до неузнаваемости. А PDF лежит тихонько в архиве ровно с тем содержимым, которое соответствует записанному во время выступления видео.

Ну что ж, не беда, жмём Ctrl+P, выбираем «Печать в PDF» и получаем то, что нужно. Делается это просто — отслеживается событие onbeforeprint и, когда оно возникает, вместо одного лишь текущего слайда рендерятся вообще все слайды. А на onafterprint, все, кроме текущего, слайды удаляются.

На этом списков возможностей пока что закончен.

Попробовать в деле $hyoo_slides очень просто. Вам потребуется readme.md с вашим контентом и картинки. Так же рядом нужно будет скопипастить index.html, который редиректнет на веб приложение и откроет вашу презентацию в нём. А так же offline.js для поддержки оффлайна.

Имейте ввиду, что этот index.html будет выдавать приложению любые файлы, доступные с того домена, куда вы всё это дело выложите. GitHub Pages — вполне удобный и безопасный вариант. Сам его использую.

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

Галерея приложений

Но о них как-нибудь потом…

Подробнее о фреймворке можно узнать на отдельной презентации. Копнуть глубже можно в презентации посвящённой ОРП. А приподнять завесу грядущего можно в презентации о квантовании вычислений.

Все они используют $hyoo_slides для отображения. Надеюсь вскоре таких презентаций станет больше.

А сейчас, давайте приоткроем капот и посмотрим, как устроено приложение, и как сделать своё аналогичное всего за один вечер.

$hyoo_slides_page $mol_view
    sub /
        <= Listener
        <= Speaker
export class $hyoo_slides_page extends $mol_view {

    sub() { return [
        this.Listener() ,
        this.Speaker() ,
    ] }

}

Перед вами верхнеуровневое описание одного экрана на языке view.tree и эквивалентный код на TypeScript. Тут мы объявляем компонент $hyoo_slides_page, который расширяет базовый компонент $mol_view. У этого компонента есть свойство sub. Всё, что возвращает это свойство, будет отрендерено внутри компонента. Поэтому мы переопределяем его, передавая в качестве значения массив из двух элементов: Listener — компонент вывода слайда слушателям и Speaker — компонент дополнительной панели докладчика.

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

sub() {

    const role = this.role()

    return [
        this.Listener() ,
        ... ( role === 'speaker' ) ? [ this.Speaker() ] : [] ,
    ]

}

Тут логика у нас простая: слайды для слушателей выводим всегда, а вот панель докладчика показываем только, если текущая роль — speaker. Если роль изменится, то и раскладка приложения тоже изменится благодаря магии объектного реактивного программирования.

Роль мы будем брать из параметра адреса, через специальный реактивный API $mol_state_arg.

role() : 'speaker' | 'listener' {
    return $mol_state_arg.value( 'role' ) || 'speaker'
}

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

Давайте опишем интерфейс слушателя.

Listener $mol_page

    title <= title

    tools /
        <= Slide_switcher

    body /
        <= Listener_content
        <= Progress

Он использует стандартный компонент $mol_page который рисует типичную страницу с шапкой и телом. В шапке есть область, куда выводится название страницы. Через свойство title можно указывать, что туда выводить. Что мы и сделали, связав его свойство title с нашим одноимённым свойством. Теперь, меняя наше свойство, мы полностью контролируем, что будет выводиться на странице в качестве заголовка.

Справа в шапке, есть область вывода дополнительных инструментов — tools. В неё мы выводим Slides_switcher — компонент для отображения номера слайда и переключения между соседними слайдами.

И, наконец, в качестве тела страницы в body мы выводим содержимое слайда и прогресс бар.

Как же реализовать Slide_switcher? Просто используем стандартный компонент $mol_paginator.

Slide_switcher $mol_paginator
    value?val <=> slide?val

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

Для отображения содержимого слайда мы воспользуемся опять же стандартным компонентом $mol_text.

Listener_content $mol_text

    uri_base <= uri_base

    text <= listener_content

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

Как вы уже, наверно, догадались для отображения прогресса тоже есть стандартный компонент — $mol_portion.

Progress $mol_portion
    portion <= progress
portion: [ 0 .. 1 ]

Скармливаем ему число от 0 до 1 и получаем заполненную на эту долю индикатор.

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

Speaker $mol_page

    head <= speaker_tools /$mol_view

    body /
        <= Speaker_content

Теперь поднимемся уровнем выше и создадим компонент приложения $hyoo_slides, который использует компонент страницы.

$hyoo_slides $mol_view

    Page!index $hyoo_slides_page
        - ...

    plugins /

        <= Nav
        <= Touch
        <= Speech_next

        - ...

У любого $mol_view компонента есть свойство plugins через которое можно подключать к нему дополнительную логику. Подключаемые плагины живут на том же DOM узле, что и сам компонент. Компонент их инициирует при своём рендеринге. А когда о перестаёт рендериться — плагины уничтожаются автоматически.

Также мы объявили свойство Page, которое для каждого индекса возвращает отдельный экземпляр разработанного нами ранее компонента $hyoo_slides_page.

Page!index $hyoo_slides_page

    role <= role

    slide?val <=> page_slide!index?val

    speaker_tools /

        <= Speech_toggle
        <= Speech_text
        <= Open_listener

Свойство role мы передаём в подкомпонент как есть. Свойство slide компонента страницы связываем двусторонней связью со свойством page_slide приложения. Обратите внимание, что page_slide принимает не только опциональное новое значение, но и индекс страницы. Это позволяет для каждой страницы возвращать свой номер. Наконец, в ранее объявленный нами слот speaker_tools мы помещаем три компонента, помогающие управлять слайдами.

Speech_toggle мы реализуем через стандартный компонент $mol_check_icon, рисующий иконку. При клике на него, он переключает флаг checked. А текущее состояние отображается изменением цвета иконки.

Speech_toggle $mol_check_icon

    Icon <= Speech_toggle_icon $mol_icon_microphone

    checked?flag <=> speech_enabled?flag

Иконку мы взяли из пакета $mol_icon, где из 4000 иконок, выполненных в аскетичном material design стиле, легко найти нужную.

Тут всё просто. Эта кнопка будет ссылкой $mol_link. Ей можно задать свойство uri с адресом, а можно поступить хитрее и просто пропатчить текущий адрес, заменив через arg некоторые параметры.

Listener_open $mol_link

    target \_blank

    arg *
        role \listener
        slide null

    sub /
        <= Listener_open_icon $mol_icon_external

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

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

plugins /

    <= Nav
    <= Touch
    <= Speech_next
    <= Speech_prev
    <= Speech_start
    <= Speech_end

    - ...

Все использованные нами плагины можно разделить на 3 категории: клавиатурная навигация, управление жестами и управление голосом.

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

Nav $mol_nav

    keys_y <= slide_keys
    keys_x <= slide_keys

    current_y?val <=> slide?val
    current_x?val <=> slide?val
slide_keys: [ 0 , 1 , 2 , 3 , ... , 30 ]
                      ^
                    slide

Для отслеживания пальцев есть плагин $mol_touch. С его помощью можно зумить, панорамировать и свайпать. Именно последняя возможность нас сейчас и интересует.

Touch $mol_touch

    swipe_to_left?event <=> go_next?event
    swipe_to_right?event <=> go_prev?event
go_next( event? : Event ) {
    this.slide( this.slide() + 1 )
}

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

Для голосового управления служит плагин $mol_speech. Необходимо создавать по экземпляру плагина на каждый вариант действия.

Sing $mol_speech

    event_catch?val <=> sing?val

    patterns /
        \sing( \S+?)*
        \спой( \S+?)*

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

По умолчанию, $mol_speech требует вежливого обращения. Например, нужно говорить не «спой», а «спой, пожалуйста». Можно переопределить свойство suffix, чтобы изменить или вообще убрать это кодовое слово.

Speech_next_auto $mol_speech

    event_catch?val <=> go_next?val

    suffix \

    patterns <= speech_next_auto_patterns

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

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


    

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

Чтобы добавить поддержку оффлайна, достаточно включить в бандл модуль mol/offline/install.

include \/mol/offline/install

Он автоматически поднимает ServiceWorker, который кеширует все запросы. А в случае недоступности сети — выдаёт данные из кеша. Это не самая крутая реализация, но для простых случаев, когда не хочется заморачиваться, — подойдёт.

Ранее я говорил, что во время печати нужно рендерить все страницы, но вручную отслеживать onbeforeprint и onafterprint нет никакой необходимости, ведь у нас же реактивное программирование в полный рост, поэтому мы воспользуемся реактивным состоянием $mol_print, дающим нам реактивный флаг, к которому мы можем подвязать рендеринг.

sub() {

    if( !this.$.$mol_print.active() ) {
        return [ this.Page( this.slide() ) ]
    }

    return $mol_range2(
        index => this.Page( index ) ,
        ()=> this.slide_keys().length ,
    )

}

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

В итоге у нас получилось современное веб приложение с кучей функциональности и весом всего в 35 кб. Добавить manifest и будет полноценное Progressive Web Application. Разумеется, многие детали не были рассмотрены. Увидеть их можно в коде на ГитХабе. По всем вопросам пишите телеграмы в чате.


Эти слайды: slides.hyoo.ru
Исходники приложения: hyoo-ru/slides.hyoo.ru.
Телеграм чат: @mam_mol

Буду рад, если вы попробуете сделать свою презентацию с помощью $hyoo_slides. И буду совсем счастлив, если поможете сделать его ещё лучше, чем есть сейчас. Спасибо за внимание!

© Habrahabr.ru