Making slides with reveal.js

(and how to make it nice)

Dougal, Gatsby tea

17 April 2018

Why?

  • Want to make talks that have both video and math?
    • PowerPoint math is a nightmare; LaTeXiT little better
    • Beamer output is PDF: video support possible, but barely
  • Many of the advantages of Beamer:
    • Git-friendly
    • Reasonable math via MathJax
      • With q_{n,i} = \frac{1}{(7 n + i)^2} , probably have \scriptsize \sum_{n=0}^\infty q_n^1 + q_n^2 - q_n^3 + q_n^4 - q_n^5 - q_n^6 \stackrel{?}{=} \frac{24}{7 \sqrt{7}} \int_{\pi/3}^{\pi/2} \! \log \left\lvert \frac{\tan t + \sqrt 7}{\tan t - \sqrt 7} \right\rvert \mathrm{d}t
  • Can also do crazy things

JavaScript is not the best…

Initial implementation written in 10 days (May 1995)

…but it's not like TeX is better

Nicifying: Pug

  • Source of talk is in HTML
    <section>
      <h2>Slide Title</h2>
      <ul>
        <li>Bullet point</li>
        <li class="fragment">
          A bold claim
          <span class="cite">[<a href="https://www.nature.com/articles/439392d">Schmidhuber
          2006</a>]</span>
        </span></li>
      </ul>
    </section>
  • Comes with markdown support, but it's frustrating
  • Better: PugJS
    section
      h2 Slide Title
      ul
        li Bullet point
        li.fragment
          | A bold claim
          +ref("https://www.nature.com/articles/439392d", "Schmidhuber 2006")
    

Pug power

- var height = 100, width = 800;
svg(height=height, width=width)
  - var l = 0, r = 1, n_pts = 100;
  - function f(x) { return x * Math.sin(x * 60) - Math.pow(x, 3.12); }
  - var xs = [], fs = [];
  - for (var i = 0; i < n_pts; i++) {
  -   x = l + i / (n_pts - 1) * r;
  -   xs.push(x * width);
  -   fs.push( (f(x) + 2) / 3 * height );
  - }
  each x, i in xs
    if i > 0
      line(x1=xs[i - 1], y1=fs[i - 1], x2=xs[i], y2=fs[i],
           style="stroke:rgb(0, 200, 0); stroke-width: 2")

Nicifying: Math

  • Default ships with "live" MathJax support
    .(style="display: none")
      $
      \DeclareMathOperator*{\E}{\mathbb E}
      \newcommand{\PP}{\mathbb P}
      $ 
    
    section
      ul
        li $\mu_\PP = \E_{X \sim \PP}[ k(X, \cdot) ]
  • Can be slow
  • Sometimes leads to rendering mistakes during presentation
  • Better: pre-render to SVG with MathJax-node
    • Browser doesn't see MathJax at all

Nicifying: Jumping around

  • Built-in "overview" mode sucks
  • reveal.js-menu
  • Wrote little functions to skip fragments:
// Functions to skip to next/previous slide
// Based on rejected PR: https://github.com/hakimel/reveal.js/pull/371
function nextSlide() {
    var idx = Reveal.getIndices();
    if (Reveal.availableRoutes().down) {
        Reveal.slide(idx.h, idx.v + 1);
    } else if (Reveal.availableRoutes().right) {
        Reveal.slide(idx.h + 1);
    }
    navigateLastFragment();
}
function prevSlide() {
    var idx = Reveal.getIndices();
    if (Reveal.availableRoutes().up) {
        Reveal.slide(idx.h, idx.v - 1, 99999);
        navigateLastFragment();
    } else if (Reveal.availableRoutes().left) {
        Reveal.slide(idx.h - 1, 99999);
        navigateLastFragment();
    }
}
function navigateLastFragment() {
    var fragments = Reveal.getCurrentSlide().querySelectorAll('.fragment');
    var maxIdx = -1;
    for (var i = 0; i < fragments.length; i++) {
        var f = parseInt(fragments[i].dataset.fragmentIndex || 0, 10);
        if (f > maxIdx) {
            maxIdx = f;
        }
    }
    Reveal.navigateFragment(maxIdx);
}

// Reveal.configure argument:
  keyboard: {
      40: 'next',  // down arrow: act like spacebar
      38: 'prev',  // up arrow: opposite of down arrow
      33: prevSlide, // page up: previous slide
      34: nextSlide  // page down: next slide
  },

Nicifying: Build layout

  • reveal.js's README says to fork the library for each talk
  • Much, much better: use npm to install it and dependencies
  • Repo only contains talk and reference to package versions
"dependencies": {
  "reveal.js": "github:dougalsutherland/reveal.js#fragment-in-url",
  "reveal.js-menu": "^1.1.1"
},
"devDependencies": {
  "chokidar": "^2.0.3",
  "express": "^4.16.3",
  "jstransformer-escape-html": "^1.1.0",
  "livereload": "^0.7.0",
  "locks": "^0.2.2",
  "lodash.template-cli": "^0.5.0",
  "mathjax-node-page": "^2.0.0",
  "minimist": "^1.2.0",
  "node-sass": "^4.8.3",
  "node-watch": "^0.5.8",
  "pug": "^2.0.3",
  "pug-plugin-debug-line": "github:pugjs/pug-plugin-debug-line",
  "serve-static": "^1.13.2",
  "split2": "^2.2.0",
  "ws": "^5.1.1"
}

Nicifying: Build system

  • Default setup uses grunt
  • Re-implementation of make that's way harder to use
    • Especially if you want to re-build figures, etc…
  • Nobody uses grunt anymore; now it's gulp, webpack, …
  • Instead: use a Makefile; it's worked since 1976
  • Wrote a little bin/compile wrapper to do Pug+MathJax
BIN := ${CURDIR}/node_modules/.bin
all: index.html css/djs.css

css/%.css: scss/%.scss
  ${BIN}/node-sass $< > $@

index.html: slides.pug layout.pug js/mj-plugin/fragments.js
  ${CURDIR}/bin/compile --livereload=${USE_LIVERELOAD} --sync=${USE_SYNC}

Nicifying: Live-reload and sync with bin/serve

  • Script watches for file edits and re-compiles
  • Runs node-livereload server, tells browser to reload
  • Runs websocket server for browser, editor to communicate
    • Shift-control click in browser opens editor at that point
    • Editor plugin to slide browser to right point
      • One line in Vim
      • Takes a Python plugin in Sublime

Drawbacks

  • Learning a new system
  • Complicated/interactive stuff is a lot of work
  • Doesn't show PDFs well; remake figures in SVG
  • ~150MB of deps per repo (in development; deploy is 5MB)
  • Nontrivial layouts can be a pain
    • Adaptive sizing makes position: absolute weird
- var width = 400; var height = Math.round(800 * 4/15);
- var base = 'display: block; position: absolute; top: 425px; left: 50%; vertical-align: middle; ';
- var pos_l = base + `margin-left: -${width + 10}px; width: ${width}px;`
- var pos_r = base + `margin-left: 10px; width: ${width}px;`
img.fragment.fade-out(       data-fragment-index="1", data-src="img/example/rep-both.svg",   style=pos_l)
img.fragment.current-visible(data-fragment-index="1", data-src="img/example/rep-d1-nys.svg", style=pos_l)
img.fragment(                data-fragment-index="2", data-src="img/example/rep-lite.svg",   style=pos_l)
img(                                  data-src="img/subspace-fits/samps.svg",    style=pos_r)
img(                                  data-src="img/subspace-fits/fit-full.svg", style=pos_r)
img.fragment(data-fragment-index="1", data-src="img/subspace-fits/basis.svg",    style=pos_r)
img.fragment(data-fragment-index="1", data-src="img/subspace-fits/fit-nys.svg",  style=pos_r)
img.fragment(data-fragment-index="2", data-src="img/subspace-fits/fit-lite.svg", style=pos_r)

Should you use it?