diff --git a/presentations/tpac-2025/Templates/Lato-Bold.woff2 b/presentations/tpac-2025/Templates/Lato-Bold.woff2 new file mode 100644 index 0000000..56fe1a2 Binary files /dev/null and b/presentations/tpac-2025/Templates/Lato-Bold.woff2 differ diff --git a/presentations/tpac-2025/Templates/Lato-BoldItalic.woff2 b/presentations/tpac-2025/Templates/Lato-BoldItalic.woff2 new file mode 100644 index 0000000..e5772ed Binary files /dev/null and b/presentations/tpac-2025/Templates/Lato-BoldItalic.woff2 differ diff --git a/presentations/tpac-2025/Templates/Lato-Italic.woff2 b/presentations/tpac-2025/Templates/Lato-Italic.woff2 new file mode 100644 index 0000000..bc81071 Binary files /dev/null and b/presentations/tpac-2025/Templates/Lato-Italic.woff2 differ diff --git a/presentations/tpac-2025/Templates/Lato-Regular.woff2 b/presentations/tpac-2025/Templates/Lato-Regular.woff2 new file mode 100644 index 0000000..b5e3d07 Binary files /dev/null and b/presentations/tpac-2025/Templates/Lato-Regular.woff2 differ diff --git a/presentations/tpac-2025/Templates/Montserrat-Black.woff2 b/presentations/tpac-2025/Templates/Montserrat-Black.woff2 new file mode 100644 index 0000000..8373b83 Binary files /dev/null and b/presentations/tpac-2025/Templates/Montserrat-Black.woff2 differ diff --git a/presentations/tpac-2025/Templates/Montserrat-BlackItalic.woff2 b/presentations/tpac-2025/Templates/Montserrat-BlackItalic.woff2 new file mode 100644 index 0000000..ac8d369 Binary files /dev/null and b/presentations/tpac-2025/Templates/Montserrat-BlackItalic.woff2 differ diff --git a/presentations/tpac-2025/Templates/Montserrat-Bold.woff2 b/presentations/tpac-2025/Templates/Montserrat-Bold.woff2 new file mode 100644 index 0000000..b463410 Binary files /dev/null and b/presentations/tpac-2025/Templates/Montserrat-Bold.woff2 differ diff --git a/presentations/tpac-2025/Templates/Montserrat-BoldItalic.woff2 b/presentations/tpac-2025/Templates/Montserrat-BoldItalic.woff2 new file mode 100644 index 0000000..e1cf3ee Binary files /dev/null and b/presentations/tpac-2025/Templates/Montserrat-BoldItalic.woff2 differ diff --git a/presentations/tpac-2025/Templates/Overview.html b/presentations/tpac-2025/Templates/Overview.html new file mode 100644 index 0000000..9590213 --- /dev/null +++ b/presentations/tpac-2025/Templates/Overview.html @@ -0,0 +1,1693 @@ + + + + + + + %%Title%% + + + + + + + + + + +
+

This is a template for slides for TPAC 2025.

+ +

Author(s): The user manual at the end has setup information and + instructions. To write slides, look at the slides in this + template.

+ +

Reader(s): + + To start the slide show, press ‘A’. Return to + the index with ‘A’ or ‘Esc’. On a touch screen, use a 3-finger + touch. Double click to open a specific slide. In slide mode, press + ‘?’ (question mark) to get a list of available commands. + + To start the slide show, press Shift+F5 + (Command+Enter on Mac). Return to the index by pressing ‘Esc’. You + can also click to open a specific slide.

+ +

If it doesn't work: Slide mode requires a + recent browser with JavaScript. If you are using the ‘NoScript’ add-on (Firefox or the + Tor Browser), or changed the ‘site settings’ (Chrome, Vivaldi, Opera, Brave and some other + browsers), or the ‘permissions for this site’ (Edge), you may have + to explicitly allow JavaScript on these slides. Internet Explorer + is not supported.

+
+ + + + +
+ + +
+ + + + + +
+ Leaving slide mode. +
+ + +
+

%%Title%%

+
%%Author%%
+

TPAC 2025
+ Kobe, Japan & online
+ 10–14 November 2025

+
+ +
+

Notes for slide 1 can be put here.

+
+ + +
+

Lists

+

This is an H3

+
    +
  1. Potatoes
  2. +
  3. Onions and olives
  4. +
+

Another H3

+ +
+ +
+

Notes for slide 2 can be put here.

+
+ + +
+

Lists with icons

+ +
+ +
+

Notes.

+
+ + +
+

Some elements

+

Words can be given a strong emphasis, which + makes them appear in bold

+

The normal emphasis has a highlighter effect.

+

Code looks like this: if (a) return b;

+

This link goes to slide 2

+

This is an example of a note, in a smaller font

+
+ +
+

Notes.

+
+ + +
+

Incremental display (1/3)

+
+
+

Unfold (default style)

+
    +
  • This item is shown right away
  • + + + +
+
+
+

Fade in (class=emerge)

+
    +
  • This item is shown right away
  • + + + +
+
+
+
+ +
+

Notes.

+
+ + +
+

Incremental display (2/3)

+
+
+

Quick
(class=quick)

+
    + + + +
+
+
+

Fade in + red items
(class="emerge strong")

+
    + + + +
+
+
+
+ +
+

Notes.

+
+ + +
+

Incremental display (3/3)

+
+
+

Quick + greeked
(class="quick greeked")

+
    + + + +
+
+
+

Fade in + red + dim (class= "emerge strong dim")

+
    + + + +
+
+
+
+ +
+

Notes.

+
+ + +
+

Elements updated in-place

+

Combining class=incremental with class=in-place + yields elements that are displayed one by one, with each one + replacing the previous. Example: +

+ +
    +
 X  +
    +
+ +
  O +
 X  +
    +
+ +
 XO +
 X  +
    +
+ +
 XO +
 X  +
 O  +
+ +
XXO +
 X  +
 O  +
+ +
XXO +
 XO +
 O  +
+ +
XXO +
 XO +
 OX +
+
+

(class=overlay is an alias for class=incremental.) +

+ +
+

Notes.

+
+ + +
+

‘Slipshow’ presentations

+

A slipshow presentation is one where you don't put each + topic on a separate slide, but add it to a long scroll, which + automatically moves up to display it.

+

The idea is by Paul-Elliot Anglès d'Auriac, see + his program on + GitHub.

+

B6+ supports a simple version.

+

This slide is an example.

+

Just progress (with space, arrow, click or swipe) to display + additional topics.

+
+

A topic

+

This new topic is added…

+
+
+

Another topic

+

When there is no more space, old content moves up…

+
+ + + + + + + + + +
Column head AColumn head B
A tableJust as an example…
A2B2
A3B3…
+
+

A topic with a list

+

The space for the list is reserved…

+
    +
  1. … but the list
  2. +
  3. is added separately…
  4. +
+
+

One more bit of content…

+

And a final one.

+
+ +
+

Notes.

+
+ + +
+

An image on the side

+ [Picture of a stylized tree with colorful, square leaves] +

It seems the famous ‘lorem ipsum’ is based on a text by Cicero, + but with the lines mixed up. On a Cicero by the text, it seems the + ‘ipsum lorem’ is famous based with lines but mixed up.

+
+ +
+

Notes.

+
+ + +
+

An image on the side

+ [Picture of a stylized tree with colorful, square
+	     leaves] +

And again, with class slide side right.

+

It seems the famous ‘lorem ipsum’ is based on a text by Cicero, + but with the lines mixed up. On a Cicero by the text, it seems the + ‘ipsum lorem’ is famous based with lines but mixed up.

+
+ +
+

Notes.

+
+ + +
+

A ‘bleeding’ image

+ [Top view of four pairs of hand holding each other crosswise] +

Adding class cover to the image makes it stretch to + the edges of the slide.

+
+ +
+

Notes.

+
+ + +
+

A ‘bleeding’ image

+ [Top view of four pairs of hand holding each other crosswise] +

And also on the right side…

+

Hint: Add class=clear on the slide to omit the banner + and slide number.

+
+ +
+

Notes.

+
+ + +
+

A big image on the side

+ [Picture of a stylized tree with colorful, square leaves] +

It seems the famous ‘lorem ipsum’ is based on a text by Cicero, + but with the lines mixed up. On a Cicero by the text, it seems the + ‘ipsum lorem’ is famous based with lines but mixed up.

+
+ +
+

Notes.

+
+ + +
+

A big image on the side

+ [Picture of a stylized tree with colorful, square leaves] +

And again, with class ‘slide side right big’.

+

It seems the famous ‘lorem ipsum’ is based on a text by Cicero, + but with the lines mixed up…

+
+ +
+

Notes.

+
+ + +
+

A big, ‘bleeding’ image

+ [Top view of four pairs of hand holding each other crosswise] +

It seems the famous ‘lorem ipsum’ is based on a text by Cicero, + but with the lines mixed up. On a Cicero by the text, it seems + ‘ipsum lorem’ is famous based with lines but mixed up.

+
+ +
+

Notes.

+
+ + +
+

A big, ‘bleeding’ image

+ [Top view of four pairs of hand holding each other crosswise] +

And on the right side…

+
+ +
+

Notes.

+
+ + +
+

A figure

+
+ + pie chart + +

The description of the pie chart, here as a table: + + + +
Name Percentage +
Andy 16% +
Chloe 21% +
Daniel 13% +
Grace 20% +
Sophia 30% +
+

+
+ +
+

Notes.

+
+ + +
+

In columns

+
+
+

Title

+

Children of an element with a class of columns are + distributed over two columns

+
+
+

Title

+

This is the second child, which goes into the right + column

+
+
+

Title

+

And this is the third one. Left column again.

+
+

Etc.

+
+
+ +
+

Notes.

+
+ + +
+
place t l
+
place t
+
place t r
+
Here is something for the left + side, with class place l
+
class place puts an element + centered in a 3×3 grid
+
place r
+
place b l
+
place b
+
combine place with + top (or t), + right (or r), + bottom (or b) and + left (or l)
+
+ +
+

Notes.

+
+ + +
+

Numbered lines of code

+
+Lines in a PRE can be numbered
+                       (automatically)
+  * Give the PRE a class of "numbered"
+  * Works for up to 20 lines
+                       (depending on font size)
+  six
+   seven
+   eight
+    nine
+    ten
+  eleven
+
+ +
+

Notes.

+
+ + +
+

Striped tables

+ + + + + + + +
row 1has no background
row 2has a gray background
row 3has no background
row 4has a gray background
etc.
etc.
+
+ +
+

Notes.

+
+ + +
+

Image overlay: cover

+ + Image: Statue of Gutenberg +

Careful, some images make the text hard to + read!

+
+ +
+

Notes.

+
+ + +
+

Image overlay: fit

+ + Image: Statue of Gutenberg +

Careful, some images make the text + hard to read!

+
+ +
+

Notes.

+
+ + +
+

White text

+ [photo with a beach, cliffs, buildings, dark clouds and a surfer] + +

(Photo ‘Surfer and + the stormy sea’ by Xavier Nohet)

+
+ +
+

Notes.

+
+ + +
+

Shout and grow!

+

Shout:

+

Takahashi method!

+

Shout & Grow:

+

Animated

+
+ +
+

Notes.

+
+ + +
+

Slide transitions

+

The style sheet predefines several transitions: +

+

A transition can be set globally, applying to all slides; +

or + locally, applying only to the transition between this slide and the + next.

+
+ +
+

Notes.

+
+ + +
+

Automatically scale text down

+

If the text doesn't fit, and you really cannot reduce it nor + split the slide, you can ask b6+ to scale the text down until it + does fit.

+

To do this, add class textfit to the slide.

+

Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ Too much text…
+ The last line.

+
+ +
+

Notes.

+
+ + +
+

Convert to PDF

+ + + + + + +
+
+

Print in portrait mode to get: +

    +
  • multiple slides per page +
  • notes between the slides +
+
+

+ Try it! [W3C team only]

+

A4 + or Letter

+
+
+
+

Print in landscape mode to get:

+
    +
  • one slide = one page +
  • no notes +
+
+

+ Try it! [W3C team only]

+

PDF + slides

+
+
+
+
+ +
+

Notes.

+
+ + +
+

Conclusion

+
    +
  1. Making slides is easy
  2. +
  3. Questions?
  4. +
+
+ +
+

Notes.

+
+ + + + +
+

User manual

+ +

Setting up your slides

+ +

This is a template for slides for TPAC 2025. It uses either + the Shower script (version 3.2) or + the b6+ + script for the presentation. (To enable Shower, uncomment the + script tag in the HTML source.)

+ +

If you cannot put them + online yourself, you can download a zip (see below) with everything + needed to develop slides offline and ask Bert Bos for help uploading the + slides once they are ready.

+ +

Developing slides online (W3C team)

+ +

Writing slides online (with JigEdit, WebDav or + CVS) is only available to the W3C team. Make a directory under https://www.w3.org/2025/Talks/TPAC/. Copy the Overview.html from https://www.w3.org/2025/Talks/TPAC/Templates/ into your + directory and edit the content, or just use it as an example.

+ +

Developing slides offline

+ +

If you develop your slides offline (or plan to + present them without a network), then download this zip file. Unpacking it creates the following directories and + files:

+ + + +

Make a directory for your own slides under + TPAC-2025. You can copy the + Overview.html file there as a starting point, or + just use it as an example. If you make any images, put them in that + directory as well.

+ +

If you are able to upload your slides, put your directory with + all that it contains under https://www.w3.org/2025/Talks/TPAC/. There is no need to + upload the Templates directory. It is is already + there.

+ +

Writing slides

+ +

Slides

+ +

Each slide is a section + element* with a class + of slide:

+ +
<section class="slide">
+  ... slide content here...
+</section>
+ +

Inside the slides, use normal HTML elements (p, ul, + em, etc.).

+ +

*) Note for advanced users: Although not + shown in this template, it is in fact possible to use other + elements than section. One common choice + is div.

+ +

Speaker notes or comments

+ +

You can add additional text, such as speaker notes or + explanations, between the slides. They will be visible in index + mode but not in slide mode. Use elements with a class + of comment:

+ +
<section class="comment">
+  ... text here...
+</section>
+ +

Slide numbers

+ +

If a slide should not have a slide number and an image banner, + add the class clear:

+ +
<section class="slide clear">
+  ... slide content here...
+</section>
+ +

On title slides, this only + removes the number, not the banner.

+ +

Adding the class clear on + the body element omits the slide number and the + side banner from all slides. + +

Title slides (cover slides)

+ +

For cover slides (the title slide or separator slides between + parts of a presentation), add a class cover. You + can combine cover and clear. + E.g.:

+ +
<section class="slide cover clear">
+  <h1>My presentations<h1>
+  <address>Peter W. Slidemaker</address>
+</section>
+ +

Final slide

+ +

The class final is meant for a last slide, + e.g., for conclusions or thanks (but it may be used elsewhere, + too):

+ +
<section class="slide final clear">
+  <h2>Conclusions<h2>
+  …
+</section>
+ +

Illustrations on the left or right

+ +

Slides with narrower text and an illustration on the left or + right can be made by adding the class side to the + slide. Inside the slide there should be exactly one element that + also has a class of side (an image or some other + element). Two sizes are possible: normal (about 1/3 of the slide) + and big (about 2/3 of the slide).

+ +

To put an image on the left:

+ +
<section class="slide side">
+  <img src="..." alt="..." class="side">
+  ... slide content here...
+</section>
+ +

To put the image on the right instead, add class right + (which may be abbreviated to r):

+ +
<section class="slide side r">
+  <img src="..." alt="..." class="side">
+  ... slide content here...
+</section>
+ +

Add class big to the slide for a bigger image. + To put the image on the left:

+ +
<section class="slide side big">
+  <img src="..." alt="..." class="side">
+  ... slide content here...
+</section>
+ +

And on the right:

+ +
<section class="slide side r big">
+  <img src="..." alt="..." class="side">
+  ... slide content here...
+</section>
+ +

The image can be stretched to the edges of the slide by adding a + class cover. The image is not deformed. It is + scaled to be big enough to cover the image area and then either the + sides are cropped (if it is too wide) or the top and + bottom*.

+ +
<section class="slide side big">
+  <img src="..." alt="..." class="side cover">
+  ... slide content here...
+</section>
+ +

*) Note for advanced users: It is + possible to indicate which sides should be cropped: add an + attribute like style="object-position: 20% 60%" to + indicate that, of the amount to be cropped from the sides, 20% + should be cropped on the left and the remaining 80% on the right; + and of the amount to be cropped from the top and bottom, 60% should + come from the top and the remaining 40% from the bottom. Thus, + e.g., ‘0% 100%’ says never to crop anything from the left (0%) if + the image is too wide, and only to crop from the top (100%) if the + image is too tall. (The default corresponds to ‘50% 50%’, i.e., + crop equal amounts from both sides.)

+ +

Figures

+ +

When information is in the form of an image (a diagram, a chart, + a screenshot, etc.), put it inside a figure + element. Add a figcaption if needed.

+ +
<figure><img src="..." alt="..."><figure>
+ +

If the image is not accessible, use a details + element instead and add a description, like this:

+ +
<details>
+ <summary><img src="..." alt="..."></summary>
+ ... the same data as in the image, but as text...
+</details>
+ +

The description becomes visible when the user clicks on the + image. (The slide above shows an image of a + pie chart that is described by a table with the same data.)

+ +

Automatic sizing of images

+ +

Setting the class autosize on an image + instructs b6+ to scale down the image if the slide's content is + otherwise too tall:

+ +
<img src="my-image.png" alt="..." class=autosize>
+ +

You can have several images with class autosize + on a slide and they are all scaled down by the same percentage.

+ +

Progress bar

+ +

If you want a progress bar during the slide presentation, add an + empty div with a class + of progress. It can be put before the first slide + or after the last, but there should be at most one such element in + the file:

+ +
<div class="progress"></div>
+ +

The progress bar will show as a thin red line along the bottom of + the slides. Its length indicates the position of the current slide + in the slide deck.

+ +

B6+ also sets a custom style + variable --progress with a value between 0 and 1 + on the body. This may be useful if you write your + own style rules for a progress indicator.

+ +

Incremental display

+ +

To progressively reveal elements on a slide, put a class + of next on all elements that should not be visible + right away. They will become visible one by one as you press the + space bar or an arrow key. E.g.:

+ +
<ul>
+  <li>This item is visible when the slide appears
+  <li class="next">This item is not immediately visible
+  <li class="next">This is the third item to appear
+</ul>
+<p class="next">This is the last element to appear
+ +

By default, each new element appears with a short animation as + if it unfolds from left to right. Two alternative animations are + available: emerge makes the elements fade in + and quick omits the animation. The class can be + set on each incremental element:

+ +
<li class="next emerge">...
+ +

or on an ancestor, e.g.:

+ +
<ul class="emerge">
+  <li class="next">...
+ +

Three optional modifiers change how elements look before, after + or while they are the currently active + element: Strong makes the currently active element + red. Greeked replaces the elements that are not + yet visible by a gray bar. (Useful to show how many elements are + still invisible.) And dim grays out the elements + that are no longer the active element.

+ +

Like the animation, these modifiers can be set on the + incremental element itself or on an ancestor. The modifiers can + also be combined, e.g.:

+ +
<ul class="emerge strong dim">
+  <li class="next">...
+ +

‘Slipshow’ presentations

+ +

With b6+ (but not with Shower) you can make slides that have + more incremental elements than fit on the slide. To display them, + the slide automatically scrolls to each element as it is + revealed.

+ +

This mode of presentation is called a ‘slipshow’ by its + inventor, Paul-Elliot Anglès d'Auriac. + (His program is on + GitHub.) B6+ only implements a simple variant.

+ +

Scrolling slides may be useful for long content or when you want + to add content while keeping some of the preceding content on + screen. (An alternative is updating elements + in place.)

+ +

Automatic slide shows

+ +

Slides can be made to advance automatically after a given time, + by setting a data-timing attribute on them with a + value of MM:SS (minutes and seconds), or a number + followed by an s (seconds), m + (minutes) or h (hours), e.g.:

+ +
<section class="slide" data-timing="1:03">
+<section class="slide" data-timing="20.5s">
+<section class="slide" data-timing="0.33m">
+ +

E.g., the last one means that the slide is shown for a maximum + of 0.33 minutes (about 20 seconds) before the next slide appears. + (You can still advance the slide by hand before that.)

+ +

If the slide contains incremental + elements, the time is distributed equally over those elements. + E.g., if there are three incremental elements, the time is divided + by four so that the first incremental appears after a quarter of + the given time, the second after half the given time, and the last + one after three quarters. You can also set a time on an individual + incremental element, in which case that time is used for that + element. E.g.:

+ +
<section class=slide data-timing="8s">
+  <p>This slide has 3 incremental elements.
+  <ol>
+    <li class=next data-timing="10s">Shown after 8/4 = 2 seconds
+    <li class=next data-timing="15s">Shown 10 seconds later
+    <li class=next>An additional 15 seconds later
+  </ol>
+</section>
+ +

After 29 seconds (= 2 + 10 + 15 + 2), the next slide will + appear.

+ +

You can set a default time on the body element, + e.g.:

+ +
<body data-timing="20.2s">
+ +

This sets the time for all slides that do not have + a data-timing attribute of their own.

+ +

Setting the data-timing attribute to 0 + indicates that the slide or element does not automatically + advance.

+ +

Two columns

+ +

To put elements side by side in two columns, make an element + (a div, ul or any other element) with + class columns. The first child of that element + will be put in the left column, the second child in the right + column. If there are more children, the third will be in the left + column again, the fourth in the right, etc.

+ +
<ul class="columns">
+  <li>First goes on the left</li>
+  <li>Second goes on the right</li>
+</ul>
+ +

Small text

+ +

Less important text can be shown in a smaller font by giving it + a class of note:

+ +
<p class="note">Note that this is harder to read</p>
+ +

Extra big text: shout

+ +

To make text extra big, give it a class + of shout, e.g.:

+ +
<p class=shout>Hurray!
+ +

(Sometimes this is referred to as + the ‘Takahashi method’: Instead of sentences or graphics, a slide + only contains one or two keywords. The narrative comes from the + speaker.)

+ +

Animated text: grow

+ +

To draw extra attention to some text or an image, it can be + animated. Adding a class of grow to it will make + it appear slowly. It will start small and one second after the + slide appears it will begin to grow and reach its normal size three + seconds later.

+ +
<p class=grow>See?
+ +

Automatic text fit

+ +

B6+ can automatically reduce the font size of a slide to fit + long text. To request this, add the class textfit + on a slide (or on the body, if all slides need + this). See the example slide.

+ +

(The way this works is that b6+ wraps the contents of the slide + in a div with the right font size. If you have + added your own style rules, make sure they still work.)

+ +

Be careful! The textfit feature can easily result in text that + is too small to read.

+ +

Automatic line numbering

+ +

Pre-formatted text (in a pre) can be given line + numbers by adding the class numbered:

+ +
<pre class="numbered">
+ +

No more than 20 lines will be numbered. (In the normal font + size, a slide fits 13 lines.)

+ +

Lists with icons

+ +

To replace the list bullets with icons, emojis, or images, make + a ul with a class of with-icons. + The first element of each li will be used instead + of the list bullet.

+ +
<ul class=with-icons>
+ <li><span>♪</span> North pole</li>
+ <li><img src="../Icons/toc.png" alt="toc button"> Metro pole</li>
+</ul>
+ +

3×3 Grid

+ +

It is possible to treat the slide as a 3×3 grid and put elements + in the four corners, in the middle of each edge, or in the center + of the slide. This is done by giving the elements a class + of place. On its own, place puts + the element in the center. By adding classes top, right, + bottom and left the element can be placed + in one of the eight other positions.

+ +
<div class="place">Put this in the center</div>
+<div class="place bottom">Put this bottom center</div>
+<div class="place top right">In the top right</div>
+ +

The direction classes can also be abbreviated to t, r, + b and l.

+ +

Image overlays (background images)

+ +

To put an image behind the text of a slide, use + an img with a class of cover:

+ +
<img class="cover" src="..." alt="...">
+ +

The image will be stretched to fill the whole of the text area. + If the image doesn't fit exactly (wrong aspect ratio), the image + will be cropped.

+ +

With a class of fit instead + of cover, the image will be scaled but without + cropping. Instead there may be white bands on the sides or + above/below the image, if it doesn't fit exactly.

+ +
<img class="fit" src="..." alt="...">
+ +

This works both for normal slides and title slides (slides with + a class of cover). The slide number + is not obscured by the image. (But you can use clear on the slide to hide it.)

+ +

It is advisable to add a class of darkmode + or lightmode to a slide with an image overlay. See + the next section.

+ +

Forcing white text or black text

+ +

The colors of the slides depend on whether ‘dark mode’ is in + effect in the operating system: slides have black text on a white + background if + dark mode is off and white on black if it is on. But when text is + overlaid on an image, it may be better to force the text to always + be white (if the image is dark) or black (on a lighter image). You + do that by setting a class on the slide: + darkmode (for white-on-black) or + lightmode (for white-on-black). E.g.:

+ +
<section class="slide darkmode">
+ +

Other colors (titles, list bullets, links, etc.) also + become fixed on such slides.

+ +

To make all slides white on black, set the + class darkmode on the body element. + In that case you can use the class lightmode on + individual slides to give them black-on-white text.

+ +

Inverting image colors in dark mode

+ +

Some images, such as diagrams, can be color-inverted and still + look good. You can give such images a + class can-invert and when the slides are displayed + in dark mode, the image colors will be inverted. (Try it on + the slide with a figure.)

+ +
<img class="can-invert" src="..." alt="...">
+ +

This also works for SVG images and other things.

+ +

Video and audio

+ +

Videos (with the video element) and audios + (with the audio element) on a slide can be made to + play automatically when the slide opens, by giving them + an autoplay attribute, e.g.:

+ +
<video src="myvideo.mp4" autoplay></video>
+ +

Videos can be used as background for the slide with + the cover or fit classes. (See + their description under ‘Image + overlays’.) The class fit is also useful + simply to make the video as large as possible if it is the only + thing on the slide.

+ +

However, if you do use one of those classes, it is best to use + the autoplay attribute and to avoid + the controls attribute, because the video controls + will capture all key presses and mouse clicks and thus you can't + use those to advance to the next slide anymore. (If you do want to + use controls, either present with + a second window (see below) or set + a data-timing attribute + on the slide, so that it advances to the next slide automatically + after a given time.)

+ +

Slide transitions

+ +

By default, each slide just replaces the previous one, but there + are several predefined slide transitions. You can set a transition + on the body element to apply it to all slides:

+ +
<body class="shower fade-in">
+ +

Or you can set it on individual slides, to apply only to the + transition between that slide and the next. (I.e., it doesn't + determine how the slide appears, but how + it disappears.)

+ +
<section class="slide wipe-left">
+ +

You can set both a global transition and local ones. The global + transition applies to slides that do not have an explicit + transition set locally.

+ +
+
fade-in
+
The new slide appears faint at first and gets more opaque + until it completely obscures the previous slide.
+ +
slide-in
+
The new slide moves in from the left, while the previous slide + moves back to the left.
+ +
slide-out
+
The current slide moves out to the left, revealing the new + slide.
+ +
move-left
+
The new slides move in from the right while the old slide + moves out to the left.
+ +
slide-up
+
The old slide moves up, revealing the new slide.
+ +
move-up
+
The old slide moves up and the new slide moves in from the + bottom.
+ +
flip-up
+
A 3D effect: the bottom of the old slide is lifted up and the + slide is turned over to reveal the new slide on its back + side.
+ +
flip-left
+
Another 3D effect, but in this case the right side of the + slide is lifted up and the slide is flipped over to the left, + revealing the new slide on the back side.
+ +
center-out
+
A small circle appears in the middle of the old slide and + reveals the new slide. The circle grows until it covers the whole + slide.
+ +
wipe-left
+
The new slide moves in from the right, until it covers the old + slide.
+ +
zigzag-left
+
A zigzag pattern moves in from the right. To the left is the + old slide, to the right the new one.
+ +
zigzag-right
+
A zigzag pattern moves in from the left. To the left is the + new slide, to the right the old one.
+ +
cut-in
+
The new slide moves in from the top left and covers the old + slide.
+ +
assemble
+
The contents of the new slide fly in from all directions.
+
+ +

Accessibility

+ +

When you present while using a screen reader, you cannot use the + screen reader's usual keystrokes to navigate, only + the keystrokes defined by the b6+ script. + However, the screen reader will speak each slide as soon as it + appears. The script creates an element with + attributes role=region + and aria-live=assertive for that purpose.

+ +

When you leave slide mode, the screen reader will say ‘stopped’. + To make it say something else (e.g., because you want a different + language than English), create an element with role=region and + aria-live=assertive yourself and put the text to speak in it. + E.g.:

+ +
<div role="region" aria-live="assertive">
+  Terminé.
+</div>
+ +

Exporting to PDF

+ +

The slides can be exported to PDF (or printed) in two ways: + multiple slides per page with comments interleaved, or one slide + per page without any comments. The latter may be useful to create a + PDF suitable for presenting, when it is not possible to use an HTML + browser.

+ +
+
portrait
When printing in portrait mode, the output + will contain as many slides per page as will fit and the comments + are printed between the slides. This corresponds to viewing the + slides in index mode.
+ +
landscape
When the output is in landscape mode, each + page consists of one slide, without page margins, and without the + comments between the slides.
+
+ +

Note: In landscape mode, the style sheet tries to set the size + of the output page to exactly the size of a slide, but not all user + agents that produce PDF respect that. (And, obviously, a printer is + limited to the available paper.) There may be some black margin to + the right and below each slide. Prince does respect the size. W3C + team can also use the ",pdfui" tool online.

+ +

Presenting

+ +

Mouse gestures and keystrokes

+ +

To present the slides, load them into a browser that supports + JavaScript and CSS and then either click the play (▶) button, press + the A key, double-click on a slide or touch the + screen with three fingers (on certain devices).

+ +

If you are using Shower instead of b6+, press + Shift+F5 (Command+Enter on Mac) + or click on a slide.

+ +

Navigate though the slides by clicking the left mouse button, + pressing the spacebar, the arrow keys or Page-up/Page-Down. The + Home and End keys jump to the first, resp. last + slide. F1 or F toggles full + screen mode. C shows a clicakble table of + contents. The ? (question mark) key shows a list + of available commands.

+ +

If you have automatically advancing + slides, you can pause the slide show with + the P key or the Play/Pause key, + if your keyboard has it. You can also navigate to a different slide + and resume from there.

+ +

To exit the presentation, press the A key or + the Esc key.

+ +

For more keys, see the documentation for + keys + & gestures in b6+ or + keyboard shortcuts in Shower.

+ +

Dark mode

+ +

While in slide mode, you can press the D (only + with b6+) key to + switch between black-on-white and white-on-black. (This temporarily + adds or removes the darkmode class + on body, see ‘Forcing white + text or black text’.) When the + computer is already in dark mode, the key instead switches the + slides to light mode (and adds the class lightmode + on the body).

+ +

Using two screens or two windows

+ +

B6+ (but not Shower) can show the slides in a second window. The + first window can then be used to control the slide show and view + notes and next slides. If you have two screens that can show + different content (e.g., your computer's screen and a projector), + you can thus present the slides on one screen, and preview + the next slide and any notes on the other.

+ +

Open the second window by pressing the 2 key, + or the button in index mode.

+ +

Drawing on the slides

+ +

After pressing the W key, you can draw on the + current slide with the mouse. Press W again to + clear the drawing.

+ +

The drawing is not permanent and there is always only one slide + with a drawing: As soon as you start drawing on another slide, the + previous slide is cleared.

+ +

The color of the drawing can be set with style rules, e.g., like + this:

+ +
.b6-canvas {color: red}
+ +

Clocks to show remaining time

+ +

When using a second screen, it is possible to show clocks on the + first screen with the remaining time, the time used so far, and the + real (wall clock) time. (The clocks are normally only shown on + the first screen, but they can be included in slides or overlaid + on slides, by adding suitable markup and/or CSS rules.)

+ +

By default, the clocks will count down from 30 minutes and show + a warning 5 minutes before the end. (In the style used + for this document, the clock turns from green to orange.) You can + set different times with the following classes:

+ +
+
duration=...
+
+

To set the duration to, e.g., 45 minutes, + add duration=45 to the class attribute of + the body. Example:

+ +
<body class="duration=45">
+
+ +
warn=...
+
+

To set the warning to, e.g., one minute, put this in the class + of the body:

+ +
<body class="warn=1">
+
+
+ +

B6+ has two kinds of clocks built-in, but also provides + primitive elements with which to build your own clock.

+ +

To get one of the built-in clocks, make an empty element with a + class of either fullclock + or clock. The former will display the real + (wall clock) time, the number of minutes so far, the number of + minutes left, a small ‘pie-chart’ showing the proportion of time + used, and four buttons: subtract one minute, add one minute, pause + the clock, and reset the clock. The simple clock will display the + pie chart and the four buttons (somewhat smaller) and the remaining + minutes.

+ +

When you make your own clock, you can make use of the following + classes and attributes:

+ +
+
hours-real, minutes-real, seconds-real
+
+

B6+ will fill all elements with a class + of hours-real with the current hour (wall-clock + time) and keep them up to date. The hour will always be two + digits and use a 24-hour clock: 00 + to 23.

+ +

Similarly, all elements with a class + of minutes-real or seconds-real + will contain the current minutes or seconds, respectively, also + always as two digits, 00 to 59. + E.g.:

+ +
<b class=hours-real>00</b>:<b class=minutes-real>00</b>
+
+ +
hours-used, minutes-used, seconds-used
+
+

Elements with these classes will contain the time since the + slides where loaded (or since the clock was reset, + see timereset below). E.g.:

+ +
<b class=minutes-used>00</b>'<b class=seconds-used>00</b>"
+
+ +
hours-remaining, minutes-remaining, seconds-remaining
+
+

Ditto, but for the time still remaining. If the used time + exceeds the duration, these times will be shown as 00.

+
+ +
timeinc, timedec
+
+

An element with a class of timeinc will act as + a button that increments the duration, and thus the remaining + time, by one minute. timedec decrements the + duration by one minute. E.g.:

+ +
<button class=timeinc>+1 min</button>
+<button class=timedec>−1 min</button>
+
+ +
timepause
+
+

An element with this class acts as a toggle to pause & + resume the clocks. When the clocks are paused, the used time does + not progress and the remaining time does not diminish. (The real + time clocks continue, of course.) When the element is clicked + again, the clocks resume. E.g.:

+ +
<button class=timepause>pause</button>
+
+ +
timereset
+
+

An element with this class acts as a button to restart the + clocks, i.e., to set the used time to zero. Example:

+ +
<button class=timereset>reset</button>
+
+
+ +

In addition to setting the time in elements with the classes + mentioned above, b6+ also updates the style property, the class and + a data- attribute on the body. This is useful for + style rules to change the styles of elements based on the progress + of the slide show. In particular, b6+ sets the following:

+ +
+
--time-factor
+
This custom style property on the body will be set to a value + between 0.0 and 1.0, representing what fraction of the duration + has been used.
+ +
data-time-factor
+
This attribute on body will be set to the + percentage of the duration already used. This will be a whole + number with two digits between 00 and 99, or 100.
+ +
time-warning
+
When the remaining time is less than the warn time, b6+ will + add this class to the classes on body.
+
+ +

Starting in slide mode

+ +

Add ‘?full’ at the end of the URL (but before any fragment ID) + to open the slides in slide mode instead of index mode.

+ +

To open in slide mode at a specific slide, add ‘?full’ and the + ID or the number of the slide, + e.g., Overview.html?full#place + or Overview.html?full#20.

+ +

Hiding the mouse pointer

+ +

When you don't want the mouse pointer to remain on the screen in + slide mode, add the class hidemouse on + the body element. If the mouse does not move for + 5 seconds, the pointer is made invisible. It comes back as soon as + the mouse is moved.

+ +
<body class="hidemouse">
+ +

You can also set a different timeout in seconds. E.g, to set a + short timeout of 1.5 seconds:

+ +
<body class="hidemouse=1.5">
+ +

Ignore mouse clicks

+ +

Normally, a mouse click anywhere on a slide (other than on a + hyperlink or form element) has the effect of advancing to the next + slide or incremental element. If you don't want that, add the + class noclick on the body + element.

+ +
<body class="noclick">
+ +

Embedding slides in other documents

+ +

You can embed a single slide in another document with the help + of an <object> or <iframe> + element. To avoid that a keypress or click + accidentally changes the slide, you can disable navigation + and index mode: add ‘?full&static’ at the + end of the URL, followed by ‘#’ and the ID or number of the desired + slide. E.g.:

+ +
<object data="Overview.html?full&static#18">...</object>
+ +

or

+ +
<iframe src="Overview.html?full&static#18"></iframe>
+ +

Adding ‘?static’ on its own to the URL is also possible: It + shows all slides in index mode and disables switching to slide + mode.

+ +

Note that using ‘?full&static’ on + an automatic slide show plays the whole + slide show without the possiblility to pause it.

+ +

Speaking guidelines

+ +

The page ‘Speaking guidelines’ of the TPAC 2025 site contains + recommendations for presenters.

+
+ + + + + + + diff --git a/presentations/tpac-2025/Templates/b6plus.js b/presentations/tpac-2025/Templates/b6plus.js new file mode 100644 index 0000000..316a56e --- /dev/null +++ b/presentations/tpac-2025/Templates/b6plus.js @@ -0,0 +1,3357 @@ +/* b6plus.js $Revision: 1.153 $ + * + * Script to simulate projection mode on browsers that don't support + * media=projection or 'overflow-block: paged' (or ‘overflow-block: + * optional-paged’, from the 2014 Media Queries draft) but do support + * Javascript. + * + * Documentation and latest version: + * + * https://www.w3.org/Talks/Tools/b6plus/ + * + * Brief usage instructions: + * + * Add the script to a page with + * + * + * + * The script assumes each slide starts with an H1 or is an element + * with class "slide". The slide must be a direct child of the BODY. + * If an H1 starts a slide, all elements until the next H1 are part of + * that slide, except for those with a class of "comment", which are + * hidden in slide mode. + * + * Elements with a class of "progress", "slidenum" or "numslides" are + * treated specially. They can be used to display progress in the + * slide show, as follows. Elements with a class of "numslides" will + * have their content replaced by the total number of slides in + * decimal. Elements with a class of "slidenum" will have their + * content replaced by the number of the currently displayed slide in + * decimal. The first slide is numbered 1. Elements with a class of + * "progress" will get a 'width' property whose value is a percentage + * between 0% and 100%, corresponding to the progress in the slide + * show: if there are M slide in total and the currently displayed + * slide is number N, the 'width' property will be N/M * 100%. + * + * There can be as many of these elements as desired. If they are + * defined as children of the BODY, they will be visible all the time. + * Otherwise their visibility depends on their parent. + * + * Usage: + * + * - Press A to toggle normal and slide mode. The script starts in + * normal mode. + * + * - Press Page-Down to go to the next slide. Press Page-Up, up arrow + * or left arrow to back-up one page. + * + * - Press Space, right arrow, down arrow or mouse button 1 to advance + * (incremental display or next slide) + * + * On touch screens, a tap with three fingers toggles slide mode, a + * wipe right goes back one slide, and wipe left advances. + * + * TODO: don't do anything if media = projection + * + * TODO: option to allow clicking in the left third of a slide to go + * back? + * + * TODO: Accessibility of the second window. + * + * TODO: Show an icon in the corner when sync mode is on? + * + * TODO: Allow a language for localized messages and clocks that is + * different from the slides' language? + * + * TODO: More or other syntaxes for commands in syncSlide()? "all" or + * "index" for "0"; "<", "previous" for "-"; ">", "next" for "+"; + * "last" for "$"... + * + * TODO: Also fill elements with class=slidenum in the preview window. + * + * TODO: In the table of contents, indicate the current slide? + * + * TODO: The help box and the table of contents explicitly stop any + * automatic slide show, but it only resumes when the user + * subsequently navigates to another slide or incremental element. + * + * TODO: Use a DIALOG for the warning box instead of a DIV? + * + * TODO: Include more ideas from "slipshow" presentations? Allow + * incrementally displayed elements to align to the top or center of + * the slide, rather than only the bottom? + * https://presentation-slipshow-4b25b8.forge.apps.education.fr/ + * + * TODO: Allow drawing on a slide in the preview window and + * automatically copy the drawing to the second window? + * + * TODO: Include a (structured and/or text) editor? + * + * TODO: When the 2nd window closes, reset the volume of audio and + * video in the 1st window to what it was before it was set to 0.01. + * + * TODO: An easy way to find out the URL of a slide when still in the + * index mode (for sharing, for bookmarking) without having to open + * the sldie fullscreen first. (Idea by Coralie.) + * + * TODO: A way to package a slide set as a single file for + * distribution. + * + * TODO: Do not set the 'width' on .progress, but rely on the style + * sheet to use the '--progress' property? + * + * TODO: Currently the speaker notes are not accessible in slide + * mode. Use the preview window for speech rather than the slide + * window? A keypress to speak the notes (while keeping them invisible)? + * + * TODO: Move repository to GitHub, to get more feedback? + * + * Originally derived from code by Dave Raggett. + * + * Author: Bert Bos + * Created: May 23, 2005 (b5) + * Modified: Jan 2012 (b5 -> b6) + * Modified: Oct 2016 (added jump to ID; fixes bugs with Home/End key handling) + * Modified: Apr 2018 (added touch events) + * Modified: May 2018 (support 'overflow-block' from Media Queries 4) + * Modified: Mar 2019 (support fixed aspect ratio, progress elements, b6 -> b6+) + * Modified: Aug 2020 (add class=visited to past elts in incremental display) + * Modified: Oct 2020 (start in slide mode if URL contains "?full") + * Modified: Apr 2021 (disable navigation if URL contains ‘?static’) + * Modified: May 2021 (rescale if window size changes while in slide mode) + * Modified: Jun 2021 (only one incremental item active, as in Shower since 3.1) + * Modified: Sep 2021 (a11y: added role=application and a live region) + * Modified: Dec 2021 (added noclick option; set slide number in URL if no ID) + * Modified: Dec 2021 (Added popup help tied to the "?" key) + * Modified: Apr 2022 (Added support for a second window, tied to the "2" key) + * Modified: Apr 2022 (forwarding of events in the second window to the first) + * Modified: Aug 2022 (help popup appears in the 2nd window if requested there) + * Modified: Nov 2022 (support server-sent events to sync slides) + * Modified: Nov 2022 (added clocks; localized to German, French and Dutch) + * Modified: Dec 2022 (protect against loading b6plus.js twice) + * Modified: Sep 2023 (show buttons in index mode to go to slide mode and more) + * Modified: Jan 2024 (swapped UI: 2nd window for slides, 1st for preview) + * Modified: Dec 2024 (ability to show a table of contents) + * Modified: Jan 2025 (data-timing attribute for automatic slide shows) + * Modified: Feb 2025 (scroll incremental elements into view: "slipshow") + * Modified: Feb 2025 (allow drawing on slides with the mouse) + * Modified: Mar 2025 (sync videos in preview window, support autoplay videos) + * Modified: Mar 2025 ("textfit" feature to reduce font size of long text) + * Modified: Mar 2025 (fullscreen automatically tries to use external screen) + * Modified: Mar 2025 ("autosize" feature to automatically reduce image size) + * Modified: Apr 2025 (hide speaker notes by default in index mode) + * Modified: Apr 2025 (Alt or Option key shows URLs of slides in index mode) + * + * Copyright 2005-2025 W3C, ERCIM + * See http://www.w3.org/Consortium/Legal/copyright-software + */ + +(function() { + +"use strict"; + +/* Localized strings */ +const translations = { + "Remaining time. To change, add class 'duration=n' to body" : { + de: "Restzeit. Um sie zu ändern, fügen Sie die Klasse 'duration=n' zu BODY hinzu", + fr: "Temps restant. Pour le changer, ajoutez la classe 'duration=n' à BODY", + nl: "Resterende tijd. Om de tijd te veranderen, voeg de class 'duration=N' toe aan BODY"}, + "min": { // Abbreviation for "minutes" + de: "Min", + fr: "min", + nl: "min"}, + "current time": { + de: "aktuelle Uhrzeit", + fr: "heure actuelle", + nl: "huidige tijd"}, + "used": { + de: "verbraucht", + fr: "utilisé", + nl: "gebruikt"}, + "remaining": { // As in "remaining time" + de: "Restzeit", + fr: "restant", + nl: "resterend"}, + "pause": { // Label on a button to pause the clock + de: "Pause", + fr: "pause", + nl: "pauze"}, + "resume": { // Label in a button to pause the clock + de: "fortsetzen", + fr: "reprendre", + nl: "hervatten"}, + "+1 min": { // Label on a button to add 1 minute + de: "+1 Min", + fr: "+1 min", + nl: "+1 min"}, + "−1 min": { // Label on a button to shorten time by 1 minute + de: "−1 Min", + fr: "−1 min", + nl: "−1 min"}, + "restart": { // Label on a button to reset the clock + de: "Neustart", + fr: "réinitialiser", + nl: "herstart"}, + "No navigation possible while sync mode is on.": { + de: "Bei aktiviertem Sync-Modus ist keine Navigation möglich.", + fr: "Aucune navigation possible lorsque le mode synchro est activé.", + nl: "Geen navigatie mogelijk terwijl de synchronisatiemodus is ingeschakeld."}, + "Press S to toggle sync mode off.": { + de: "Drücken Sie S, um den Sync-Modus auszuschalten.", + fr: "Appuyez sur S pour désactiver le mode synchro.", + nl: "Druk op S om de synchronisatiemodus uit te schakelen."}, + "Synchronization error.": { + de: "Synchronisierungsfehler", + fr: "Erreur de synchronisation.", + nl :"Synchronisatiefout."}, + "You can try to turn synchronization back on with the S key.": { + de: "Sie können versuchen, die Synchronisation mit der Taste S wieder einzuschalten.", + fr: "Vous pouvez essayer de réactiver la synchronisation avec la touche S.", + nl: "U kunt proberen de synchronisatie weer in te schakelen met de S-toets."}, + "An error occurred while trying to switch into fullscreen mode": { + de: "Beim Wechsel in den Vollbildmodus ist ein Fehler aufgetreten", + fr: "Une erreur s'est produite en essayant de passer en mode plein écran", + nl: "Er is een fout opgetreden bij het overschakelen naar volledig scherm"}, + "Fullscreen mode is not possible": { + de: "Der Vollbildmodus ist nicht möglich", + fr: "Le mode plein écran est impossible", + nl: "Volledig scherm is niet mogelijk"}, + "You can try again with the F or F1 key.": { + de: "Sie können es mit der Taste F oder F1 erneut versuchen.", + fr: "Vous pouvez réessayer avec la touche F ou F1.", + nl: "U kunt het opnieuw proberen met de toets F of F1."}, + "Syncing turned OFF.\nPress S to turn syncing back on.": { + de: "Synchronisierung ausgeschaltet.\nDrücken Sie S, um die Synchronisierung wieder einzuschalten.", + fr: "Synchronisation désactivée\nAppuyez sur S pour réactiver la synchronisation,", + nl: "Synchroniseren uitgeschakeld\nDruk op S om het synchroniseren weer in te schakelen"}, + "Syncing turned ON\nPress S to turn syncing off": { + de: "Synchronisierung eingeschaltet\nDrücken Sie S, um die Synchronisierung auszuschalten", + fr: "Synchronisation activée\nAppuyez sur S pour désactiver la synchronisation", + nl: "Synchronisatie ingeschakeld\nDruk op S om synchronisatie uit te schakelen"}, + "Stopped.": { + de: "Gestoppt.", + fr: "Arrêté.", + nl: "Gestopt."}, + "Mouse & keyboard commands": { + de: "Maus- und Tastaturbefehle", + fr: "Commandes de la souris et du clavier", + nl: "Muis- en toetsenbordopdrachten"}, + "A, double click, 3-finger touch": { + de: "A, Doppelklick, 3-Finger-Touch", + fr: "A, double clic, toucher à 3 doigts", + nl: "A, dubbelklik, 3-vinger touch"}, + "enter slide mode": { + de: "Dia-Modus einschalten", + fr: "passer en mode diapo", + nl: "naar de diamodus gaan"}, + "A, Esc, 3-finger touch": { + de: "A, Esc, 3-Finger-Touch", + fr: "A, Esc, toucher à 3 doigts", + nl: "A, Esc, 3-vinger touch"}, + "leave slide mode": { + de: "Dia-Modus ausschalten", + fr: "quiter le mode diapo", + nl: "diamodus verlaten"}, + "space, , , swipe left": { + de: "Leertaste, , , links wischen", + fr: "espace, , , glisser vers la gauche", + nl: "spatie, , , veeg naar links", + }, + "space, , , click": { + de: "Leertaste, , , click", + fr: "espace, , , clic", + nl: "spatie, , , klik"}, + "next slide or incremental element": { + de: "nächstes Dia oder inkrementelles Element", + fr: "diapo suivante ou élément incrémentiel", + nl: "volgende dia of incrementeel element"}, + "PgDn": {}, + "PgDn, swipe left": { + de: "PgDn, links wischen", + fr: "PgDn, glisser vers la gauche", + nl: "PgDn, veeg naar links"}, + "next slide": { + de: "nächstes Dia", + fr: "diapo suivante", + nl: "volgende dia"}, + "PgUp, , , swipe right": { + de: "PgUp, , , rechts wischen", + fr: "PgUp, , , glisser vers la droite", + nl: "PgUp, , , veeg naar rechts"}, + "previous slide": { + de: "vorheriges Dia", + fr: "diapo précédente", + nl: "vorige dia"}, + "End": {}, + "last slide": { + de: "letztes Dia", + fr: "dernière diapo", + nl: "laatste dia"}, + "Home": {}, + "first slide": { + de: "erstes Dia", + fr: "première diapo", + nl: "eerste dia"}, + "F1, F": {}, + "toggle fullscreen mode": { + de: "Vollbildmodus umschalten", + fr: "basculer le mode plein écran", + nl: "volledig scherm aan/uit", + }, + "2": {}, + "C": {}, + "show slides in 2nd window": { + de: "abspielen in 2. Fenster", + fr: "lire dans 2e fenêtre", + nl: "afspelen in 2e venster"}, + "?": {}, + "this help": { + de: "diese Hilfe", + fr: "cette aide", + nl: "deze hulp"}, + "S": {}, + "toggle sync mode on/off": { + de: "Sync-Modus ein-/ausschalten", + fr: "activer/désactiver le mode synchro", + nl: "sync-modus aan/uit"}, + "(More information in the b6+ manual)": { + de: "(Weitere Informationen im b6+ Handbuch)", + fr: "(Plus d'informations dans le manuel de b6+)", + nl: "(Meer informatie in de b6+ handleiding)"}, + "▶\uFE0E": {}, + "play slides or stop playing": { + de: "Dias abspielen oder halten", + fr: "lancer les diapos ou arrêter", + nl: "dia's afspelen of stoppen"}, + "play/stop": { + de: "abspielen/halten", + fr: "lire/arrêter", + nl: "afspelen/stoppen"}, + "⧉": {}, + "play in 2nd window": { + de: "abspielen in 2. Fenster", + fr: "lire dans 2eme fenêtre", + nl: "afspelen in 2de venster"}, + "play/stop slides in a 2nd window": { + de: "Dias abspielen/halten in einem zweiten Fenster", + fr: "lancer/arrêter les diapos dans une 2eme fenêtre", + nl: "dia's afspelen/stoppen in een 2de venster"}, + "❮": {}, + "back": { + de: "zurück", + fr: "précédent", + nl: "terug"}, + "❯": {}, + "forward": { + de: "vorwärts", + fr: "suivant", + nl: "vooruit"}, + "?": {}, + "help": { + de: "Hilfe", + fr: "aide", + nl: "help"}, + "◑": {}, + "dark mode": { + de: "Dunkel­modus", + fr: "mode sombre", + nl: "donkere modus"}, + "toggle dark mode on/off": { + de: "Dunkelmodus ein- oder ausschalten", + fr: "activer ou désactiver le mode sombre", + nl: "schakel de donkere modus aan of uit"}, + "notes": { + de: "Notizen", + fr: "notes", + nl: "notities"}, + "(slide mode) table of contents
(index mode) show/hide notes": { + de: "(Dia-modus) Inhaltsverzeichnis
(Indexmodus) Notizen anzeigen/ausblenden", + fr: "(en mode diapo) table des matières
(en mode index) afficher/masquer les notes", + nl: "(diamodus) inhoudsopgave
(index-modus) notities weergeven/verbergen"}, + "P, ": {}, + "pause/resume automatic slide show": { + de: "anhalten/fortsetzen der automatischen Dias", + fr: "pause/reprise du diapo automatique", + nl: "pauzeer/hervat automatisch afspelen"}, + "W": {}, + "start/stop drawing on the slide": { + de: "zeichnen auf dem Dia ein-/ausschalten", + fr: "dessiner sur la diapo activer/désactiver", + nl: "tekenen op de dia aan/uit"}, + "F and F1 only work in the window with the slides. (Browser security restriction.)": { + de: "F und F1 funktonieren nur im Fenster mit den Dias. (Browsersicherheitsbeschränkung.)", + fr: "F et F1 ne fonctionnent que dans la fenêtre avec les diapo. (Restriction de sécurité du navigateur.)", + nl: "F en F1 werken alleen in het venster met de dia's. (Browser-beveiligingsbeperking.)"}, + "Alt, Option": {}, + "(index mode) show URL of slide": { + de: "(Indexmodus) URL des Dias anzeigen", + fr: "(en mode index) afficher l'URL de la diapo", + nl: "(index-modus) toon URL van dia"}, + "🗊": {}, + "show/hide notes": { + de: "Notizen anzeigen/ausblenden", + fr: "Afficher/masquer les notes", + nl: "Notities weergeven/verbergen"}, +}; + +/* Logo for use on a dark background. (The border of the circle is + * light violet.) */ +const logo = 'b6+'; + +/* Initial inner size of the second window. */ +const popupWidth = 800, popupHeight = 690; + +/* A random number for the URL of the 2nd window. */ +const randomnumber = Math.trunc(0x100000 * Math.random()).toString(36); + +/* Global variables */ +var curslide = null; +var slidemode = false; // In slide show mode or normal mode? +var switchInProgress = false; // True if waiting for finishToggleMode() +var incrementals = null; // Array of incrementally displayed items +var gesture = {}; // Info about touch/pointer gesture +var numslides = 0; // Number of slides +var stylesToLoad = 0; // # of load events to wait for +var limit = 0; // A time limit used by toggleMode() +var interactive = true; // Allow navigating to a different slide? +var fullmode = false; // Whether "?full" was in the URL +var progressElts = []; // Elements with class=progress +var slidenumElts = []; // Elements with class=slidenum +var numslidesElts = []; // Elements with class=numslides +var liveregion = null; // Element [role=region][aria-live=assertive] +var savedContent = ""; // Initial content of the liveregion +var noclick = 0; // If != 0, mouse clicks do not advance slides +var hideMouseTime = null; // If set, hide idle mouse pointer after N ms +var helptext = null; // List of keyboard and mouse commands +var toctext = null; // Table of contents +var hideMouseID = null; // ID of timer to hide the mouse pointer +var singleClickTimer = null; // Timeout to distinguish single & double click +var secondwindow = null; // Optional second window for slides +var firstwindow = null; // The window that opened this one +var syncmode = false; // Sync mode +var syncURL = null; // URL of sync server +var eventsource = null; // Sync server object +var startTime = 0; // Start time, used by displayed clocks +var pauseStartTime = 0; // 0 = clocks not paused, > 0 = start of pause +var clockElts = null; // Elements with class=clock +var fullclockElts = null; // Elements with class=fullclock +var realHoursElts = null; // Elements with wallclock time: hours +var realMinutesElts = null; // Elements with wallclock time: minutes +var realSecondsElts = null; // Elements with wallclock time: seconds +var usedHoursElts = null; // Elements with used time: hours +var usedMinutesElts = null; // Elements with used time: minutes +var usedSecondsElts = null; // Elements with used time: seconds +var leftHoursElts = null; // Elements with remaining time: hours +var leftMinutesElts = null; // Elements with remaining time: minutes +var leftSecondsElts = null; // Elements with remaining time: seconds +var clockTimer = 0; // Interval timer for clocks +var duration = 30 * 60 * 1000; // Default duration of a presentation 30 min +var warnTime = 5 * 60 * 1000; // Warn 5 minutes before end of duration +var language = null; // Language for localization +var switchFullscreen = false; // True = toggle fullscreen but not slide mode +var hasDarkMode = false; // Style sheet supports class=darkmode? +var incrementalsBehavior = "symmetric"; // [Experimental] +var slideTimer = null; // Timer for automatically advancing slides +var slideTiming = 0; // Default time to advance slides, 0 means off +var slideTimerPaused = false; // True = do not advance slides automatically +var loopSlideShow = false; // Whether to wrap around to the first slide +var scale = 1; // How much to scale a slide to fill the screen +var canvas = null; // Canvas for drawing on slides +var canvasContext = null; // Drawing context for the canvas +var canvasX = 0; // Most recent mouse position on the canvas... +var canvasY = 0; // ... used for drawing lines. +var hoverOverlay = null; // For showing the URL of a slide in index mode +var mouseX = 0, mouseY = 0; // Last known mouse pointer location +var commentsVisible = true; // If speaker notes are currently visible +var commentsWereVisible; // If notes were visible when 2nd window opened +var commentsDefault = false; // If speaker notes should start out visible +var visibleSlide; // The last slide used in toggleComments() +var forceClocks = false; // Update clocks even when they are paused +var clocksUpdateRequested = false; // True if a call is queued + + +/* _ -- return translation for text, or text, if none is available */ +function _(text) +{ + return translations[text]?.[language] ?? text; +} + + +/* generateID -- make sure elt has a unique ID */ +function generateID(elt, slide) +{ + var nextid = 0; // For generating unique IDs + + /* This doesn't guarantee that elt has a unique ID, but only that it + * is the first element in the document that has this ID. Which + * should be enough to make this element scroll into view when it is + * the target... */ + if (!elt.id) elt.id = "s" + slide.b6slidenum + while (document.getElementById(elt.id) !== elt) + elt.id = "s" + slide.b6slidenum + "-" + ++nextid +} + + +/* cloneNodeWithoutID -- deep clone a node, but not any ID attributes */ +function cloneNodeWithoutID(elt) +{ + var clone, h; + + clone = elt.cloneNode(false); + if (elt.nodeType === 1 /*Node.ELEMENT_NODE*/) { + clone.removeAttribute("id"); // If any + for (h = elt.firstChild; h; h = h.nextSibling) + clone.appendChild(cloneNodeWithoutID(h)); // Recursive + } + return clone; +} + + +/* updateClocks -- update clock elements */ +function updateClocks() +{ + var now, s0, m0, h0, s1, m1, h1, s2, m2, h2, used, left, factor; + + // This function is called in an animation frame, set by + // requestClocksUpdate(). + + now = new Date(); + + s0 = now.getSeconds(); + m0 = now.getMinutes(); + h0 = now.getHours(); + + for (const e of realHoursElts) + e.textContent = h0.toString().padStart(2, "0"); + for (const e of realMinutesElts) + e.textContent = m0.toString().padStart(2, "0"); + for (const e of realSecondsElts) + e.textContent = s0.toString().padStart(2, "0"); + + // Only uodate clocks if they aren't paused, or if an update is forced. + if (pauseStartTime === 0 || forceClocks) { + + if (forceClocks) forceClocks = false; // Reset + + used = now.getTime() - startTime; + s1 = Math.trunc(used / 1000); + if (usedHoursElts.length != 0) { // Used hours are displayed + h1 = Math.trunc(s1 / 60 / 60); s1 -= h1 * 60 * 60; + m1 = Math.trunc(s1 / 60); s1 -= m1 * 60; + } else if (usedMinutesElts.length != 0) { // No hours, but minutes are shown + m1 = Math.trunc(s1 / 60); s1 -= m1 * 60; + } + for (const e of usedHoursElts) + e.textContent = h1.toString().padStart(2, "0"); + for (const e of usedMinutesElts) + e.textContent = m1.toString().padStart(2, "0"); + for (const e of usedSecondsElts) + e.textContent = s1.toString().padStart(2, "0"); + + left = Math.max(0, duration - used); + s2 = Math.trunc(left / 1000); + if (leftHoursElts.length != 0) { // Remaining hours are displayed + h2 = Math.trunc(s2 / 60 / 60); s2 -= 60 * 60 * h2; + m2 = Math.trunc(s2 / 60); s2 -= 60 * m2; + } else if (leftMinutesElts.length) { // No hours, but minutes are shown + m2 = Math.trunc(s2 / 60); s2 -= 60 * m2; + } + for (const e of leftHoursElts) + e.textContent = h2.toString().padStart(2, "0"); + for (const e of leftMinutesElts) + e.textContent = m2.toString().padStart(2, "0"); + for (const e of leftSecondsElts) + e.textContent = s2.toString().padStart(2, "0"); + + // Set a precise factor 0.0..1.0 in a CSS variable on all clock elements. + // Set an integer percentage 00..100 in a data attribute on BODY. + // If time left is <= warnTime, set class=time-warning on BODY. + factor = 1 - left/duration; + for (const e of clockElts) e.style.setProperty('--time-factor', factor); + for (const e of fullclockElts) e.style.setProperty('--time-factor', factor); + document.body.setAttribute("data-time-factor", + Math.trunc(100 * factor).toString().padStart(2, "0")); + if (left <= warnTime) document.body.classList.add("time-warning"); + else document.body.classList.remove("time-warning"); + } +} + + +/* requestClocksUpdate -- queue a call to updateClocks() if not already done */ +function requestClocksUpdate() +{ + if (clocksUpdateRequested) return; + clocksUpdateRequested = true; + requestAnimationFrame(() => {clocksUpdateRequested = false; updateClocks()}); +} + + +/* addMinute -- add 1 minute to the duration */ +function addMinute(ev) +{ + duration += 60000; + + if (firstwindow) + firstwindow.postMessage({event: "duration", v: duration}); + else if (secondwindow?.closed === false) + secondwindow.postMessage({event: "duration", v: duration}); + + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* subtractMinute -- subtract 1 minute from the duration */ +function subtractMinute(ev) +{ + duration = Math.max(0, duration - 60000); + + if (firstwindow) + firstwindow.postMessage({event: "duration", v: duration}); + else if (secondwindow?.closed === false) + secondwindow.postMessage({event: "duration", v: duration}); + + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* pauseTime -- pause or resume the clocks */ +function pauseTime(ev) +{ + if (pauseStartTime) { // We're resuming, add paused time to startTime + startTime += Date.now() - pauseStartTime; + pauseStartTime = 0; + document.body.classList.remove("paused"); + } else { // We're pausing, remember start time of pause + pauseStartTime = Date.now(); + document.body.classList.add("paused"); + } + + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + + if (firstwindow) { + firstwindow.postMessage({event: "startTime", v: startTime}); + firstwindow.postMessage({event: "pauseStartTime", v: pauseStartTime}); + } else if (secondwindow?.closed === false) { + secondwindow.postMessage({event: "startTime", v: startTime}); + secondwindow.postMessage({event: "pauseStartTime", v: pauseStartTime}); + } + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* resetTime -- restart the clock */ +function resetTime(ev) +{ + startTime = Date.now(); + + if (firstwindow) + firstwindow.postMessage({event: "startTime", v: startTime}); + else if (secondwindow?.closed === false) + secondwindow.postMessage({event: "startTime", v: startTime}); + + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* ignoreEvent -- cancel an event */ +function ignoreEvent(ev) +{ + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* initClocks -- find and initialize clock elements */ +function initClocks() +{ + var t; + + // Get the duration and warn time of the presentation from body.class. + for (const c of document.body.classList) { + if ((t = c.match(/^duration=([0-9.]+)$/))) duration = 1000 * 60 * t[1]; + if ((t = c.match(/^warn=([0-9.]+)$/))) warnTime = 1000 * 60 * t[1]; + } + + // Find and remember any clock elements. Turn the live + // HTMLCollection into a static array for efficiency, because we + // don't expect new clock elements to be created. + fullclockElts = Array.from(document.getElementsByClassName("fullclock")); + clockElts = Array.from(document.getElementsByClassName("clock")); + + // If there are elements with class=fullclock or class=clock + // and that don't have child elements already, fill them with + // appropriate elements to make a clock. + for (const c of fullclockElts) { + c.setAttribute("aria-label", "clock"); + if (!c.firstElementChild) + c.insertAdjacentHTML("beforeend", '' + _('current time') + '' + + '' + + '' + + '' + _('used') + '' + + '' + + '' + _('remaining') + '' + + '' + + '' + + '' + + '' + + ''); + } + for (const c of clockElts) { + c.setAttribute("aria-label", "clock"); + if (!c.firstElementChild) + c.insertAdjacentHTML("beforeend", + '' + + '' + + '' + + '' + + '' + + ''); + } + + // Find all elements that will contain time. + realHoursElts = Array.from(document.getElementsByClassName("hours-real")); + realMinutesElts = Array.from(document.getElementsByClassName("minutes-real")); + realSecondsElts = Array.from(document.getElementsByClassName("seconds-real")); + usedHoursElts = Array.from(document.getElementsByClassName("hours-used")); + usedMinutesElts = Array.from(document.getElementsByClassName("minutes-used")); + usedSecondsElts = Array.from(document.getElementsByClassName("seconds-used")); + leftHoursElts = Array.from(document.getElementsByClassName("hours-remaining")); + leftMinutesElts = Array.from(document.getElementsByClassName("minutes-remaining")); + leftSecondsElts = Array.from(document.getElementsByClassName("seconds-remaining")); + + // Find all elements that adjust the clock and install event handlers. + for (const e of document.getElementsByClassName("timeinc")) { + e.addEventListener("click", addMinute, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + for (const e of document.getElementsByClassName("timedec")) { + e.addEventListener("click", subtractMinute, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + for (const e of document.getElementsByClassName("timepause")) { + e.addEventListener("click", pauseTime, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + for (const e of document.getElementsByClassName("timereset")) { + e.addEventListener("click", resetTime, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + + // Install a timer to update the clock elements once per second, if needed. + if (realHoursElts.length || realMinutesElts.length || + realSecondsElts.length || usedHoursElts.length || + usedMinutesElts.length || usedSecondsElts.length || + leftHoursElts.length || leftMinutesElts.length || + leftSecondsElts.length) + clockTimer = setInterval(requestClocksUpdate, 1000, false); + + // Remember start time of presentation. + if (clockTimer) startTime = Date.now(); +} + + +/* initIncrementals -- find incremental elements in current slide */ +function initIncrementals() +{ + var e = curslide; + + // Collect all incremental elements into array incrementals. + // + // The functions nextSlideOrElt() and previousSlideOrElt() maintain + // the following invariant: If there are incrementals, there is at + // most one of them with a class of "active". If there is an + // "active" element, all incrementals before it, and only those, + // have a class of "visited". If there is an "active" element, + // incrementals[incrementals.cur] points to that element; if there + // is not, incrementals.cur is -1. + // + // incrementalsBehavior is an experimental variable to evaluate + // different behaviors when going backwards inside a slide with + // incremental elements: + // + // "freeze": When you leave a slide, the incremental elements that + // are currently displayed become frozen. When going back to that + // slide, those elements are still displayed but can no longer be + // removed by pressing the left arrow. This is the behavior of + // Shower. + // + // "reset": Every time you enter a slide, all incremental elements + // are in their hidden state. E.g., if you leave a slide with all + // elements visible and then go back, all elements are hidden again. + // + // "symmetric": When you return to a slide, the slide is exactly as + // you left it. Incremental elements that were displayed when you + // left the slide are still displayed and can be hidden by pressing + // the left arrow. This is currently the default. + // + // "forwardonly": When you enter a slide, all incremental elements + // are in their hidden state (as with "reset"). In addition, + // pressing the left arrow when some incremental elements are + // displayed, resets all elements to their hidden state. + // + // Note that with all of these except "symmetric", the left arrow + // acts very much like the PageUp key: when you go back to the + // previous slide, every next press of the left arrow goes back one + // slide. + // + incrementals = []; + incrementals.cur = -1; + do { + /* Go to the next node, in document source order. */ + if (e.firstChild) { + e = e.firstChild; + } else { + while (e && !e.nextSibling) e = e.parentNode; + if (e) e = e.nextSibling; + } + if (!e) break; /* End of document */ + if (e.nodeType != 1) continue; /* Not an element */ + if (isStartOfSlide(e)) break; /* Reached the next slide */ + if (e === liveregion) break; /* Do not search in the liveregion */ + + if (e.classList.contains("incremental") || e.classList.contains("overlay")) + for (const c of e.children) + if (incrementalsBehavior === "symmetric") { + if (c.classList.contains("active")) + incrementals.cur = incrementals.length; // Start at this element + incrementals.push(c); + } else if (incrementalsBehavior === "reset" || + incrementalsBehavior == "forwardonly") { + c.classList.remove("active"); + c.classList.remove("visited"); + incrementals.push(c); + } else { // "freeze" + if (!c.classList.contains("visited") && + !c.classList.contains("active")) + incrementals.push(c); + } + if (e.classList.contains("next")) { /* It is an incremental element */ + if (incrementalsBehavior === "symmetric") { + if (e.classList.contains("active")) + incrementals.cur = incrementals.length; // Start at this element + incrementals.push(e); + } else if (incrementalsBehavior === "reset" || + incrementalsBehavior == "forwardonly") { + e.classList.remove("active"); + e.classList.remove("visited"); + incrementals.push(e); + } else { // "freeze" + if (!e.classList.contains("visited") && + !e.classList.contains("active")) + incrementals.push(e); + } + } + } while (1); +} + + +/* isStartOfSlide -- check if element has class=slide, page-break or is an H1 */ +function isStartOfSlide(elt) +{ + return elt.b6slidenum !== undefined || // shortcut: we already numbered it + (elt.nodeType == 1 && // it is an element + elt.parentNode == document.body && + (elt.classList.contains("slide") || + getComputedStyle(elt).getPropertyValue('page-break-before')=='always' || + elt.nodeName == "H1")); +} + + +/* updateProgress -- update the progress bars and slide numbers, if any */ +function updateProgress() +{ + var p = curslide.b6slidenum / numslides; + + /* Set the width of the progress bars */ + for (const e of progressElts) e.style.width = 100 * p + "%"; + + /* Set the content of .slidenum elements to the current slide number */ + for (const e of slidenumElts) e.textContent = curslide.b6slidenum; + + /* Set a custom variable on BODY for use by style rules */ + document.body.style.setProperty('--progress', p); +} + + +/* initProgress -- unhide .progress, .slidenum and .numslides elements */ +function initProgress() +{ + var s; + + /* Find all elements that are progress bars, unhide them. */ + if (interactive) { + progressElts = Array.from(document.getElementsByClassName("progress")); + for (const e of progressElts) + if (typeof e.b6savedstyle === "string") e.style.cssText = e.b6savedstyle; + } + + /* Find all that should contain the current slide number, unhide them. */ + slidenumElts = Array.from(document.getElementsByClassName("slidenum")); + for (const e of slidenumElts) + if (typeof e.b6savedstyle === "string") e.style.cssText = e.b6savedstyle; + + /* Unhide all elements that contain the # of slides. */ + for (const e of numslidesElts) + if (typeof e.b6savedstyle === "string") e.style.cssText = e.b6savedstyle; +} + + +/* numberSlides -- count slides, number them, and make sure they have IDs */ +function numberSlides() +{ + var s; + + // Count slides and make sure all slides have an ID. + numslides = 0; + for (const h of document.body.children) + if (isStartOfSlide(h)) { + h.b6slidenum = ++numslides; // Save number in element + generateID(h, h); // If the slide has no ID, add one + for (const v of h.querySelectorAll('VIDEO, AUDIO')) + generateID(v, h); // Make sure all video elts have an ID + } + + // Set content of all elements with class=numslides to the number of slides. + numslidesElts = Array.from(document.getElementsByClassName("numslides")); + for (const e of numslidesElts) e.textContent = numslides; + + // Set the # of slides in a CSS counter on the BODY. + s = window.getComputedStyle(document.body).getPropertyValue("counter-reset"); + if (s === "none") s = ""; else s += " "; + document.body.style.setProperty('counter-reset',s + 'numslides ' + numslides); +} + + +/* scaleImagesIfNeeded -- maybe shrink images with class=autosize */ +function scaleImagesIfNeeded(slide) +{ + var height, images, heights = [], f1, f2, f, saveHeight, saveMaxHeight; + + // Only handle slides that consist of one element. + // TODO: Also handle other styles of markup. + if (! slide.classList.contains('slide')) return; + + // Get all images to scale in the current slide, if any. + images = slide.querySelectorAll('img.autosize'); + if (images.length === 0) return; + + // Get the slide's height, which presumably is the desired height. + height = slide.getBoundingClientRect().height; + + // Save the slide's inline height and max-height properties, if + // any, so we can restore them later. Then set them temporarily to + // auto and none. + saveHeight = slide.style.height; + saveMaxHeight = slide.style.maxHeight; + slide.style.height = 'auto'; + slide.style.maxHeight = 'none'; + + // Get the height of the slide again, to see if it is bigger now. + if (slide.getBoundingClientRect().height > height) { + + // Get the natural height of all images. Use the height of the + // slide if the image has none. + images.forEach((e, i) => heights[i] = e.naturalHeight || height); + + // Find a factor 0.01 < f < 1.0 to multiply the height of each + // image by such that all slide content fits on the slide. (It may + // not be possible to find such a height: there may be too much + // non-image stuff on the slide already, or the author may have + // put class=autofit on images that don't influence the slide + // height.) + f1 = 0.01; + f2 = 1.0; + while (f2 > 1.00001 * f1) { // Until within 0.001% + f = (f1 + f2)/2; + images.forEach((e, i) => { + e.style.height = (f * heights[i]) + "px"}); + if (slide.getBoundingClientRect().height > height) f2 = f; else f1 = f; + } + } + + // Restore the element's style sttribute. + slide.style.height = saveHeight; + slide.style.maxHeight = saveMaxHeight; +} + + +/* scaleFontIfNeeded -- if the slide has class=textfit, maybe shrink the font */ +function scaleFontIfNeeded(slide) +{ + var style, height, f, f1, f2, wrapper, saveHeight, saveMaxHeight; + + // The slide only needs to be checked if it has class=textfit or if + // the body has that class. And then only if the slide is one + // element. TODO: Also try to handle other markup? + if (! slide.classList.contains('slide') || + (! document.body.classList.contains('textfit') && + ! slide.classList.contains('textfit'))) return; + + // Get the slide's height, which presumably is the desired height. + height = slide.getBoundingClientRect().height; + + // Save the slide's inline height and max-height properties, if + // any, so we can restore them later. Then set them temporarily to + // auto and none. + saveHeight = slide.style.height; + saveMaxHeight = slide.style.maxHeight; + slide.style.height = 'auto'; + slide.style.maxHeight = 'none'; + + // Get the height of the slide again, to see if it is bigger now. + if (slide.getBoundingClientRect().height > height) { + // Make a wrapper for the slide's content, if we didn't already. + // We'll then set a smaller font size on the wrapper to try and + // reduce the height of the slide. + wrapper = slide.firstChild; + if (!wrapper?.b6textfitwrapper) { + wrapper = document.createElement('div'); + wrapper.setAttribute('class', 'b6textfitwrapper'); + wrapper.style.display = 'contents'; // This DIV does not generate a box + while (slide.firstChild) wrapper.appendChild(slide.firstChild); + slide.appendChild(wrapper); + wrapper.b6textfitwrapper = true; + } + + // Find new font size between 1% and 100% of the current size. + f1 = 0.01; + f2 = 1.0; + while (f2 > 1.00001 * f1) { // Until within 0.001% + f = (f1 + f2)/2; + wrapper.style.fontSize = 100 * f + '%'; + if (slide.getBoundingClientRect().height > height) f2 = f; else f1 = f; + } + wrapper.style.setProperty('--font-scale-factor', f); + } + + // Restore the element's style sttribute. + slide.style.height = saveHeight; + slide.style.maxHeight = saveMaxHeight; +} + + +/* textFit -- if the body or a slide has a class=textfit, make text fit */ +function textFit() +{ + for (const h of document.body.children) + if (isStartOfSlide(h)) scaleFontIfNeeded(h); +} + + +/* autosize -- shrink images with class=autosize if needed */ +function autosize() +{ + for (const h of document.body.children) + if (isStartOfSlide(h)) scaleImagesIfNeeded(h); +} + + +/* instrumentVideos -- add event handlers to all video and audio elements */ +function instrumentVideos() +{ + // Stop any videos and audios that have an autoplay attribute, but + // remember the attribute, so we can start the video/audio when + // its slide is shown. + for (const v of document.querySelectorAll('VIDEO, AUDIO')) { + v.b6autoplay = v.autoplay; + v.autoplay = false; + v.pause(); + } + + // If a second window is open, these event handlers help to + // synchronize the playback of videos and audios in both windows. + // When the user starts, pauses or seeks a video in one window, a + // message is sent to the other window to start, seek or pause the + // video there, too. See message() for how the message is handled in + // the receiving window. + for (const v of document.querySelectorAll('VIDEO, AUDIO')) + if (v.id !== "") { + v.addEventListener('pause', ev => { + if (ev.target.b6pausing) { // We paused because of a message + ev.target.b6pausing = false; + } else { + firstwindow?.postMessage({event: 'pause', id: v.id}, '*'); + secondwindow?.postMessage({event: 'pause', id: v.id}, '*'); + } + }); + v.addEventListener('play', ev => { + if (ev.target.b6playing) { // We started play because of a message + ev.target.b6playing = false; + } else { + firstwindow?.postMessage({event: 'play', id: v.id}, '*'); + secondwindow?.postMessage({event: 'play', id: v.id}, '*'); + } + }); + v.addEventListener('seeked', ev => { + if (ev.target.b6seeking) { // We seeked as a result of a message + ev.target.b6seeking = false; + } else { + firstwindow?.postMessage({event: 'seeked', id: v.id, + v: ev.target.currentTime}, '*'); + secondwindow?.postMessage({event: 'seeked', id: v.id, + v: ev.target.currentTime}, '*'); + } + }); + v.addEventListener('volumechange', ev => { + // We only sync muted state, not volume (which is 0 in 1st window) + firstwindow?.postMessage({event: 'volumechange', id: ev.target.id, + v: ev.target.muted}, '*'); + secondwindow?.postMessage({event: 'volumechange', id: ev.target.id, + v: ev.target.muted}, '*'); + }); + } +} + + +/* hideMouse -- make the mouse pointer invisible (only in slide mode) */ +function hideMouse() +{ + if (slidemode) document.body.style.cursor = 'none'; + hideMouseID = 0; // 0 = timer has fired, cursor is hidden +} + + +/* hideMouseReset -- event handler for mousemove to reset the hideMouse timer */ +function hideMouseReset() +{ + if (hideMouseID === 0) { // Timer has fired and hid the cursor. Unhide it. + document.body.style.cursor = null; + hideMouseID = null; // null = cursor is visible + } else if (hideMouseID !== null) { // Timer hasn't fired yet. Remove it. + clearTimeout(hideMouseID); + hideMouseID = null; // null = cursor is visible + } + + /* If still in slide mode, set a new timer; otherwise remove ourselves. */ + if (slidemode) hideMouseID = setTimeout(hideMouse, hideMouseTime); + else document.removeEventListener('mousemove', hideMouseReset); +} + + +/* initHideMouse -- set a timeout to hide the mouse pointer when it is idle */ +function initHideMouse() +{ + if (hideMouseTime === null) return; + + /* Add handler to restart the timer when the mouse moves. */ + document.addEventListener('mousemove', hideMouseReset); + + /* Remove old timer, unhide cursor if hidden, start new timer. */ + hideMouseReset(); +} + + +/* rewindVideos -- reset any autoplaying videos on the current slide */ +function rewindVideos() +{ + for (const v of curslide.querySelectorAll('VIDEO, AUDIO')) + if (v.b6autoplay) { + v.currentTime = 0; + v.play(); + } +} + + +/* stopVideos -- stop any videos and audios on the current slide */ +function stopVideos() +{ + for (const v of curslide.querySelectorAll('VIDEO, AUDIO')) v.pause(); +} + + +/* displaySlide -- make the current slide visible */ +function displaySlide() +{ + var h, url, m; + + /* curslide has class=slide, page-break-before=always or is an H1 */ + curslide.style.cssText = curslide.b6savedstyle; + curslide.classList.add("active"); // Compatibility with Shower + liveregion.innerHTML = ""; // Make it empty + + if (!curslide.classList.contains('slide')) { + liveregion.appendChild(cloneNodeWithoutID(curslide)); + /* Unhide all elements until the next slide. And copy the slide to + the live region so that it is spoken */ + for (h = curslide.nextSibling; h && ! isStartOfSlide(h); h = h.nextSibling) + if (h !== liveregion) { + if (h.nodeType === 1) h.style.cssText = h.b6savedstyle; + liveregion.appendChild(cloneNodeWithoutID(h)); + } + + } else { // class=slide + /* Copy the contents of the slide to the live region so that it is spoken */ + for (h = curslide.firstChild; h; h = h.nextSibling) + liveregion.appendChild(cloneNodeWithoutID(h)); + } + + updateProgress(); + initIncrementals(); + + /* If there is a first window, tell it to scroll to the same slide. */ + if (firstwindow) + firstwindow.postMessage({event: "slide", v: curslide.id}, "*"); + + /* Update the URL displayed in the location bar. */ + history.replaceState({}, "", "#" + curslide.id) + + /* Remove any existing slide timer. Then, unless the automatic slide + * show is paused, check if the slide has a data-timing attribute, + * or failing that, use the default (from the BODY). If the result + * is not 0, set a timeout. */ + clearTimeout(slideTimer); + if (! slideTimerPaused && + (m = curslide.dataset.timing !== undefined ? + timeToMillisec(curslide.dataset.timing) / (incrementals.length + 1) : + slideTiming / (incrementals.length + 1))) + slideTimer = setTimeout(nextSlideOrElt, m); + + /* If there are any autoplay videos or audios, start them. */ + rewindVideos(); +} + + +/* hideSlide -- make the current slide invisible */ +function hideSlide() +{ + var h; + + if (!curslide) return; + + /* If any videos are playing, stop them. */ + stopVideos(); + + /* curslide has class=slide, page-break-before=always or is an H1 */ + curslide.classList.remove("active"); // Compatibility with Shower + curslide.classList.add("visited"); // Compatibility with Shower + curslide.style.visibility = "hidden"; + curslide.style.position = "absolute"; + curslide.style.top = "0"; + for (h = curslide.nextSibling; h && ! isStartOfSlide(h); h = h.nextSibling) + if (h.nodeType === 1 /*Node.ELEMENT_NODE*/ && h !== liveregion) { + h.style.visibility = "hidden"; + h.style.position = "absolute"; + h.style.top = "0"; + } +} + + +/* makeCurrent -- hide the previous slide, if any, and display elt */ +function makeCurrent(elt) +{ + console.assert(elt); + if (curslide != elt) { + hideSlide(); + curslide = elt; + displaySlide(); + } +} + + +/* fullscreen -- toggle fullscreen mode or turn it on ("on") or off ("off") */ +async function toggleFullscreen(onoff) +{ + var s, x; + + switchFullscreen = true; // For the fullscreenchange event handler + + if (onoff !== "on" && document.fullscreenElement) + document.exitFullscreen(); + else if (onoff !== "off" && document.fullscreenEnabled) + try { + // If there is exactly one external screen, use that. + s = "getScreenDetails" in window && + (x = (await getScreenDetails()).screens.filter((h) => !h.isInternal)) && + x.length == 1 ? x[0] : screen; + await document.documentElement.requestFullscreen({navigationUI: "hide", + screen: s}); + } catch (err) { + alert(_("An error occurred while trying to switch into fullscreen mode") + + ' (' + err.message + ' – ' + err.name + ")\n\n" + + _("You can try again with the F or F1 key.")); + } + else if (onoff !== "off") + window.alert(_("Fullscreen mode is not possible")); +} + + +/* createHelpText -- fill the helptext element with help text */ +function createHelpText() +{ + var iframe, button; + + /* Put the help text in an IFRAME so it is not affected by the slide style */ + iframe = document.createElement('iframe'); + iframe.setAttribute('title', _("Mouse & keyboard commands")); + iframe.srcdoc = + "" + + "" + + "" + + "" + _("Mouse & keyboard commands") + "" + + "" + + "" + + "" + + (syncmode ? "" : + "
" + + "" + + logo + " " + _("Mouse & keyboard commands") + "
" + _("A, double click, 3-finger touch") + + "" + _("enter slide mode") + + "
" + _("A, Esc, 3-finger touch") + + "" + _("leave slide mode") + + "
" + + (noclick ? + _("space, , , swipe left") : + _("space, , , click")) + + "" + _("next slide or incremental element") + + "
" + + (noclick ? _("PgDn") : _("PgDn, swipe left")) + + "" + _("next slide") + + "
" + + _("PgUp, , , swipe right") + + "" + _("previous slide") + + "
" + _("End") + + "" + _("last slide") + + "
" + _("Home") + + "" + _("first slide") + + "
" + _("F1, F") + + "" + _("toggle fullscreen mode") + + "
" + _("2") + "" + + _("show slides in 2nd window") + + (!hasDarkMode ? "" : + "
" + _("D") + + "" + _("toggle dark mode on/off")) + + "
" + _("C") + + "" + _("(slide mode) table of contents
(index mode) show/hide notes") + + "
" + _("W") + + "" + _("start/stop drawing on the slide") + + "
" + _("?") + + "" + _("this help") + + "
" + _("Alt, Option") + + "" + _("(index mode) show URL of slide") + + (!slideTiming ? "" : + "
" + _("P, ") + + "" + _("pause/resume automatic slide show"))) + + (!syncURL ? "" : + "
" + _("S") + + "" + _("toggle sync mode on/off")) + + "
" + + "

" + _("(More information in the b6+ manual)"); + iframe.style.cssText = 'margin: 0; border: none; padding: 0; ' + + 'width: 100%; height: 100%'; + button = document.createElement('button'); + button.innerHTML = "\u274C\uFE0E"; // Cross mark + button.style.cssText = 'position:absolute; top: 0; right: 16px'; + button.addEventListener('click', + ev => {helptext.remove(); ev.stopPropagation()}); + // Unfortunately, when in fullscreen mode, the Escape key is + // captured by the browser to exit fullscreen mode and we never get + // it. + button.setAttribute("tabindex", 0); + button.addEventListener('keydown', ev => { + if (ev.key == "Escape") {helptext.remove(); ev.stopPropagation()}}); + helptext = document.createElement('div'); + helptext.appendChild(iframe); + helptext.appendChild(button); + helptext.style.cssText = 'position: fixed; width: 100%; height: 100%; ' + + 'top: 0; left: 0; z-index: 2; background: #000; color: #FFF; ' + + 'text-align: center; visibility: visible'; + // In fullscreen mode, the Escape key is captured by the browser to + // leave fullscreen mode, so only the second Escape press closes the + // help text. But let's add it anyway. + helptext.setAttribute("tabindex", 0); + helptext.addEventListener('keydown', ev => { + if (ev.key == "Escape") {helptext.remove(); ev.stopPropagation()}}); +} + + +/* help -- show information about available interactive commands */ +function help() +{ + // Works both on first and second windows + clearTimeout(slideTimer); + if (!helptext) createHelpText(); + document.body.appendChild(helptext); + helptext.lastChild.focus(); // The button +} + + +/* getSlideTitle -- get the title of the slide that starts at elt */ +function getSlideTitle(elt) +{ + var title; + + if (elt.nodeType == 1 && elt.nodeName.match(/^H[1-6]$/)) + return elt.innerHTML; + else if (elt.firstChild && (title = getSlideTitle(elt.firstChild))) + return title; + else if (elt.nextSibling && !isStartOfSlide(elt.nextSibling)) + return getSlideTitle(elt.nextSibling); + else + return null; +} + + +/* createTOCText -- fill the toctext element with the table of contents */ +function createTOCText() +{ + var button, style, items = ""; + + /* Collect the titles of all slides and make them A elements inside LI. */ + for (const h of document.body.children) + if (isStartOfSlide(h)) { + let i = '#' + h.id.replaceAll('&', '&').replaceAll('"', '"'); + let t = getSlideTitle(h); // Returns an HTML fragment or null + items += '

  • ' + (t ?? '#' + h.b6slidenum) + ''; + } + + /* Make toctext a DIALOG with class "toc" containing an OL with the links. */ + toctext = document.createElement('dialog'); + toctext.classList.add('toc'); // Allow style sheet to style it + toctext.innerHTML = '
      ' + items + '
    '; + + /* A button to close the TOC. */ + button = document.createElement('button'); + button.setAttribute('autofocus', ''); + button.innerHTML = "\u274C\uFE0E"; // Cross mark + button.addEventListener('click', + ev => {toctext.close(); ev.stopPropagation()}); + toctext.prepend(button); + + /* When clicking a link in the TOC, also remove the TOC. */ + for (const e of toctext.getElementsByTagName('A')) + e.addEventListener('click', ev => toctext.close()); + + /* The C key works like the esc key and closes the table of contents. */ + toctext.addEventListener('keydown', + ev => {if (ev.key == 'c') toctext.close()}); + + document.body.append(toctext); +} + + +/* tableOfContents -- pop-up a table of contents */ +function tableOfContents() +{ + clearTimeout(slideTimer); + if (!toctext) createTOCText(); + toctext.style.visibility = 'visible'; // May have been hidden by toggleMode() + toctext.showModal(); +} + + +/* openSecondWindow -- open a 2nd window with the same slides */ +function openSecondWindow() +{ + var url; + + console.assert(!firstwindow); // We're on the first window + + // If we're in slide mode, go back to index mode. + // This should never happen: the "2" key is refused in slide mode + // and the button is invisible in slide mode. + if (slidemode) toggleMode(); + + // Open a second window if there isn't one yet. The + // "?b6window=random" avoids that this document replaces the + // original slides in the browser cache. + if (secondwindow == null || secondwindow.closed) { + url = new URL(location); + url.searchParams.delete("full"); + url.searchParams.delete("static"); + url.searchParams.delete("sync"); + url.searchParams.set("b6window", randomnumber); + // url.hash = ""; + secondwindow = open(url, "b6+ slide window", + `innerWidth=${popupWidth},innerHeight=${popupHeight},resizable`); + secondwindow.focus(); + } + + // Set the volume of any videos and audios is this window to almost + // 0. Not 0, because that sets the mute button, white we want the + // mute button in this window to still function to mute/unmute the + // video in the second window. + for (const v of document.querySelectorAll('VIDEO, AUDIO')) v.volume = 0.01; + + // The second window will send us an "init" message when it is + // ready. At that point we'll send it some information about our + // clocks and the currently active slide, if any. See message() + // below. +} + + +/* warnSyncMode -- alert the user that sync mode is on */ +function warnSyncMode() +{ + console.assert(!firstwindow); // We're on the first window + warningBanner(_("No navigation possible while sync mode is on."), "\n", + _("Press S to toggle sync mode off.")); +} + + +/* warnFullscreen -- alert the user fullscreen does not work between windows */ +function warnFullscreen() +{ + console.assert(!firstwindow); // We're on the first window + warningBanner(_("F and F1 only work in the window with the slides. (Browser security restriction.)")); +} + + +/* warningBanner -- briefly show a banner with a warning */ +function warningBanner(...content) +{ + var banner, elt; + + banner = document.createElement("div"); + banner.style = "position: fixed; left: 0; right: 0; z-index: 2;\ + text-align: center; font-size: 2vh; font-weight: bold;\ + white-space: pre-line;\ + font-family: sans-serif; margin: 0; padding: 0.5em; border-style: none;\ + background: hsla(0,0%,0%,0.6); color: hsl(0,0%,100%);\ + text-shadow: 1px 1px 1px #000, 1px 1px 1px #000; opacity: 1.0;\ + transition: opacity 3.0s"; + if (slidemode) {banner.style.top = "auto"; banner.style.bottom = "0";} + else {banner.style.top = "0"; banner.style.bottom = "auto";} + banner.append(...content); + document.body.append(banner); + + // First let the style transition fade the dialog, then remove it. + setTimeout(function () {banner.style.opacity = "0.0"}, 3000); + setTimeout(function () {banner.remove()}, 6000); +} + + +/* errorSyncMode -- show an error message when synchronization fails */ +function errorSyncMode(ev) +{ + warningBanner(_("Synchronization error."), "\n", + _("You can try to turn synchronization off and on again with the S key.")); +} + + +/* tryToggleSync -- toggle sync mode on or off, if possible */ +function tryToggleSync() +{ + console.assert(!firstwindow); // We're on the 1st window + + if (!syncURL) return; // No sync server defined + + if (syncmode) { + eventsource.close(); + syncmode = false; + secondwindow?.postMessage({event: "sync-off"}); + warningBanner(_("Syncing turned OFF.\nPress S to turn syncing back on.")); + } else { + eventsource = new EventSource(syncURL); + // Listen both for "message" events (the default type) and "page" events + eventsource.addEventListener("message", syncHandler); + eventsource.addEventListener("page", syncHandler); + eventsource.addEventListener("error", errorSyncMode); + // eventsource.addEventListener("open", function (ev) {)}); + // Don't wait for the "open" event. It seems some browsers (Safari + // and Firefox, but not Vivaldi) don't emit the event until much + // later. (When the first message arrives?) + syncmode = true; + secondwindow?.postMessage({event: "sync-on"}); + warningBanner(_("Syncing turned ON\nPress S to turn syncing off")); + } +} + + +/* unpauseAutomaticSlides -- resume a paused automatic slide show */ +function unpauseAutomaticSlides() +{ + console.assert(slideTimerPaused); + console.assert(slidemode); + slideTimerPaused = false; + requestAnimationFrame(() => { + displaySlide(); // TODO: wasteful, only start timer instead? + document.body.classList.remove('manual'); // The style may show an indicator + }); +} + + +/* pauseAutomaticSlides -- pause an automatic slide show */ +function pauseAutomaticSlides() +{ + console.assert(slidemode); + console.assert(!slideTimerPaused); + clearTimeout(slideTimer); + slideTimerPaused = true; + // Set a class, so that the style can use status indicators + requestAnimationFrame(() => document.body.classList.add('manual')); +} + + +/* toggleAnnotate -- show or hide a canvas on which you can draw */ +function toggleAnnotate() +{ + if (!canvas) { + // Canvas not yet created. Initialize it. + // TODO: Can the canvas scroll with the slide in slipshow mode? + canvas = document.createElement("canvas"); + canvas.setAttribute('class', 'b6-canvas'); + canvas.setAttribute('width', window.innerWidth); + canvas.setAttribute('height', window.innerHeight); + canvas.style.position = "fixed"; + canvas.style.top = "0"; + canvas.style.left = "0"; + canvas.style.visibility = "hidden"; + curslide.append(canvas); + canvasContext = canvas.getContext("2d"); + canvasContext.lineCap = "round"; + canvasContext.lineWidth = 2; + canvasContext.strokeStyle = + window.getComputedStyle(canvas).getPropertyValue('color'); + noclick |= 2; + // Add an event listener that follows the mouse and draws lines + canvas.addEventListener('mousemove', (e) => { + if (e.buttons & 1 == 1 && canvasX !== null) { + canvasContext.beginPath(); + canvasContext.moveTo(canvasX, canvasY); + canvasContext.lineTo(e.offsetX, e.offsetY); + canvasContext.stroke(); + } + canvasX = e.offsetX; + canvasY = e.offsetY; + }); + } + if (canvas.style.visibility == "hidden" || + canvas.parentNode !== curslide) { + // Canvas was hidden, by user pressing "w" or switching slides (or + // it was just created). + canvasContext.clearRect(0, 0, canvas.getBoundingClientRect().width, + canvas.getBoundingClientRect().height); // Clear the canvas + curslide.append(canvas); // Move it to current slide, if needed + canvasContext.strokeStyle = window.getComputedStyle(canvas) + .getPropertyValue('color'); // Get the color set in the style + canvas.style.visibility = null; // Make sure canvas is displayed + noclick |= 2; // Don't let clicks advance the slide + canvasX = null; // Mouse coordinates not yet reliable + } else { + // Canvas was in use. Hide it. + canvas.style.visibility = "hidden"; + noclick &= !2; // Reset noclick to what it was before + } +} + + +/* keyDown -- handle key presses on the BODY element */ +function keyDown(event) +{ + // We only handle the key if it is not directed at a focused element. + if (event.target.tagName !== "BODY") return; + + if (event.key === 'Alt' && !syncmode && !slidemode) { + showURL(); + return; + } + + // We don't handle other keys when a modifier key is pressed. + if (event.altKey || event.ctrlKey || event.metaKey) return; + + switch (event.key) { + case "PageDown": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) nextSlide() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow?.postMessage({event: "keydown", v: event.key}); + break; + case "PageUp": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) previousSlide() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "Spacebar": // Some older browsers + case " ": // Fall through + case "Right": // Some older browsers + case "ArrowRight": // Fall through + case "Down": // Some older browsers + case "ArrowDown": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) nextSlideOrElt() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "Left": // Some older browsers + case "ArrowLeft": // Fall through + case "Up": // Some older browsers + case "ArrowUp": // Fall through + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) previousSlideOrElt() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "Home": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) firstSlide() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "End": + if (syncmode) warnSyncMode() + else if (slidemode) lastSlide() + else if (secondwindow?.closed !== false) return + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "a": // Accepted even when not in slide mode + if (syncmode) warnSyncMode() + else if (secondwindow?.closed !== false) toggleModeAndFullscreen() + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "f": // Fall through + case "F1": + if (slidemode) toggleFullscreen() // In slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else warnFullscreen(); + break; + case "Esc": // Some older browsers + case "Escape": + if (syncmode) warnSyncMode() + else if (slidemode) toggleModeAndFullscreen() + else if (secondwindow?.closed !== false) return + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "2": + if (slidemode) return; // Only one window can be in slide mode + else if (syncmode) warnSyncMode() + else if (!firstwindow) openSecondWindow(); + else return; // We're on the 2nd window. Ignore key + break; + case "?": + help(event); + break; + case 'c': + if (syncmode) warnSyncMode() + else if (toctext?.open) toctext.close(); + else if (slidemode) tableOfContents(event); + else if (secondwindow?.closed !== false) toggleComments() // index mode + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case 'p': + case 'MediaPlay': + /* Note that unpausing calls displaySlide(), which resets the time. */ + if (syncmode) warnSyncMode() + else if (slideTimerPaused) unpauseAutomaticSlides() + else if (slidemode) pauseAutomaticSlides() + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case 's': + if (syncURL) tryToggleSync() // On 1st window, sync server defined + else if (!firstwindow) return // On 1st window, but no sync server + else firstwindow.postMessage({event: "keydown", v: event.key}); + break; + case 'd': + if (slidemode) toggleDarkMode() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case 'w': + if (syncmode) warnSyncMode() + else if (slidemode) toggleAnnotate() + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + default: + return; // Other keys have their normal meaning. + } + + event.preventDefault(); +} + + +/* load -- handle the load event */ +function load(e) +{ + if (stylesToLoad) stylesToLoad--; + e.target.removeEventListener(e.type, load); +} + + +/* toggleMedia -- swap styles for projection and screen */ +function toggleMedia() +{ + var i, h, s, links, styles; + + var re1 = /\(\s*overflow-block\s*:\s*(optional-)?paged\s*\)/gi; + var sub1 = "(overflow-block: scroll)"; + var re2 = /\(\s*overflow-block\s*:\s*scroll\s*\)/gi; + var sub2 = "(overflow-block: paged)"; + var re3 = /\bprojection\b/gi; + var sub3 = "screen"; + var re4 = /\bscreen\b/gi; + var sub4 = "projection"; + + /* Swap projection and screen in MEDIA attributes of LINK elements */ + links = document.getElementsByTagName("link"); + for (i = 0; i < links.length; i++) + if (links[i].rel === "stylesheet" && links[i].media) { + if (re1.test(links[i].media)) s = links[i].media.replace(re1, sub1); + else s = links[i].media.replace(re2, sub2); + if (re3.test(s)) s = s.replace(re3, sub3); + else s = s.replace(re4, sub4); + if (s != links[i].media) { + stylesToLoad++; + links[i].addEventListener('load', load, false); + links[i].media = s; + } + } + + /* Swap projection and screen in MEDIA attributes of STYLE elements */ + styles = document.getElementsByTagName("style"); + for (i = 0; i < styles.length; i++) + if (styles[i].media) { + if (re1.test(styles[i].media)) s = styles[i].media.replace(re1, sub1); + else s = styles[i].media.replace(re2, sub2); + if (re3.test(s)) s = s.replace(re3, sub3); + else s = s.replace(re4, sub4); + if (s != styles[i].media) { + stylesToLoad++; + styles[i].addEventListener('load', load, false); + styles[i].media = s; + } + } + + /* Swap projection and screen in the MEDIA pseudo-attribute of the style PI */ + for (h = document.firstChild; h; h = h.nextSibling) + if (h.nodeType === 7 && h.target === "xml-stylesheet") { + if (re1.test(h.data)) s = h.data.replace(re1, sub1); + else s = h.data.replace(re2, sub2); + if (re3.test(s)) s = s.replace(re3, sub3); + else s = s.replace(re4, sub4); + if (s != h.data) { + stylesToLoad++; + h.addEventListener('load', load, false); // TODO: possible? + h.data = s; + } + } +} + + +/* scaleBody -- if the BODY has a fixed size, scale it to fit the window */ +function scaleBody() +{ + var w, h, w2, h2; + + if (document.body.offsetWidth && document.body.offsetHeight) { + w = document.body.offsetWidth; + h = document.body.offsetHeight; + w2 = window.visualViewport.width * window.visualViewport.scale; + h2 = window.visualViewport.height * window.visualViewport.scale; + scale = Math.min(w2/w, h2/h); + // console.log(`scaleBody ${w}x${h} -> ${w2}x${h2} -> ${scale}`); + document.body.style.transform = "scale(" + scale + ")"; + document.body.style.position = "relative"; + document.body.style.marginLeft = (w2 - w)/2 + "px"; + document.body.style.marginTop = (h2 - h)/2 + "px"; + document.body.style.top = "0"; + document.body.style.left = "0"; + /* --shower-full-scale is for style sheets written for Shower 3.1: */ + document.body.style.setProperty('--shower-full-scale', '' + scale); + } +} + + +/* shrinkWindow -- give the 2nd window the same aspect ratio as the slides */ +function shrinkWindow() +{ + var r, w, h, safari_fix; + + // Only do somthing if this is a 2nd window, the body has a definite + // height, and we're not in fullscreen mode. + if (firstwindow && document.body.offsetWidth && document.body.offsetHeight && + ! document.fullscreenElement) { + r = document.body.getBoundingClientRect(); + if (window.visualViewport) { + w = window.visualViewport.width * window.visualViewport.scale; + h = window.visualViewport.height * window.visualViewport.scale; + } else { + w = window.innerWidth; + h = window.innerHeight; + } + safari_fix = popupWidth / w; // Safari reports 941.1 instead of 800?? + resizeBy((r.width - w) * safari_fix, (r.height - h) * safari_fix); + } +} + + +/* finishToggleMode -- finish switching to slide mode */ +function finishToggleMode() +{ + if (stylesToLoad != 0 && Date.now() < limit) { + + setTimeout(finishToggleMode, 100); // Wait some more + + } else if (stylesToLoad == 0 && Date.now() < limit) { + + limit = 0; + setTimeout(finishToggleMode, 100); // Wait 100ms for styles to apply + + } else { + + stylesToLoad = 0; + scaleBody(); // If the BODY has a fixed size, scale it to fit the window + initProgress(); // Find and initialize progress bar, etc. + initHideMouse(); // If requested, hide an idle mouse pointer + shrinkWindow(); // If 2nd window, try and remove black bands + + // If we're a 2nd window, inform the 1st window that we are ready. + if (firstwindow) firstwindow.postMessage({event: "init"}); + + /* curslide can be set if we reenter slide mode or if doubleClick set it. */ + if (curslide) displaySlide(); + else if (location.hash) targetSlide(location.hash.substring(1)); + if (!curslide) firstSlide(); + + /* There may be tall objects. Make sure we show the start of the + * slide. But only if we're not inside an object, embed or iframe, + * otherwise the outer document will be scrolled to that object or + * iframe. */ + if (! document.body.classList.contains('framed')) + document.body.scrollIntoView(); + + /* If the slide overflows, make the last of the incrementals visible. */ + scrollSlide(); + + switchInProgress = false; // Done with the mode switch + } +} + + +/* toggleMode -- toggle between slide show and normal display */ +function toggleMode() +{ + /* Do nothing if we are still in the process of switching to slide mode */ + if (switchInProgress) return; + + if (! slidemode) { + switchInProgress = true; + slidemode = true; + document.body.classList.add("full"); // Set .full on BODY + document.body.setAttribute("role", "application"); // Hint to screenreaders + + /* Make all children of BODY invisible. */ + for (const h of document.body.children) { + if (h.b6savedstyle === undefined) + h.b6savedstyle = h.style.cssText; // Remember properties + h.style.visibility = "hidden"; + h.style.position = "absolute"; + h.style.top = "0"; + h.style.left = "0"; + } + + /* Except that the liveregion is visible, but cropped. */ + liveregion.style.visibility = "visible"; + liveregion.style.clip = "rect(0 0 0 0)"; + liveregion.style.clipPath = "rect(0 0 0 0)"; // Since 'clip' is deprecated + + /* Swap style sheets for projection and screen. */ + document.body.b6savedstyle = document.body.style.cssText; // Save properties + toggleMedia(); // Swap style sheets + + /* Wait 100ms before calling a function to do the rest of the + initialization of slide mode. That function will wait for the + style sheets to load, but no longer than until limit, i.e., 3 + seconds */ + limit = Date.now() + 3000; + setTimeout(finishToggleMode, 100); + + } else { + + /* Stop any videos. */ + stopVideos(); + + /* If there is a first window, tell it we're not in slide mode anymore. */ + if (firstwindow) firstwindow.postMessage({event: "noslide"}); + + // If we're a second window, just disappear now. + if (firstwindow) window.close(); + + /* If slides are advancing automatically, stop the timer. */ + clearTimeout(slideTimer); + + /* savedContent is what a screen reader should say on leaving slide mode */ + liveregion.innerHTML = savedContent; + + /* If there was a canvas, remove it. */ + if (canvas) {canvas.remove(); canvas = null;} + + /* Unhide all children again, except comments if commentsVisible is false */ + for (const h of document.body.children) + if (typeof(h.b6savedstyle) === "string") + h.style.cssText = h.b6savedstyle; + if (! commentsVisible) { + toggleComments(); + toggleComments(); + } + + toggleMedia(); // Swap style sheets + document.body.style.cssText = document.body.b6savedstyle; // Restore style + document.body.classList.remove("full"); // Remove .full from BODY + document.body.removeAttribute("role"); // Remove "application" + curslide?.classList.remove("active"); // Remove styling + + slidemode = false; + + /* Put current slide in the URL, so the index view can highlight it. */ + if (curslide) location.replace("#" + curslide.id); + } +} + + +/* toggleModeAndFullscreen -- switch fullscreen slide mode and index mode */ +function toggleModeAndFullscreen() +{ + toggleMode(); + toggleFullscreen(slidemode ? "on" : "off"); +} + + +/* toggleDarkMode -- add, remove or toggle darkmode/lightmode on the BODY */ +function toggleDarkMode(onoff) +{ + var darkmodeIsOn, lightmodeIsOn; + + if (! hasDarkMode) return; + + // If onoff is "on", set a class on body. If onoff is "off", unset + // it. If onoff is undefined, toggle the class. If the OS is in dark + // mode, this toggles class=lightmode. Otherwise this toggles + // class=darkode. + // + if (matchMedia('(prefers-color-scheme: dark)').matches) { + // The OS is currently in dark mode. + lightmodeIsOn = document.body.classList.contains("lightmode"); + if (lightmodeIsOn && onoff !== "on") { + document.body.classList.remove("lightmode"); + firstwindow?.postMessage({event: "lightmodeOff"}); + secondwindow?.postMessage({event: "lightmodeOff"}); + } else if (! lightmodeIsOn && onoff !== "off") { + document.body.classList.remove("darkmode"); + document.body.classList.add("lightmode"); + firstwindow?.postMessage({event: "lightmodeOn"}); + secondwindow?.postMessage({event: "lightmodeOn"}); + } + } else { + // The OS is currently not in dark mode. + darkmodeIsOn = document.body.classList.contains("darkmode"); + if (darkmodeIsOn && onoff !== "on") { + document.body.classList.remove("darkmode"); + firstwindow?.postMessage({event: "darkmodeOff"}); + secondwindow?.postMessage({event: "darkmodeOff"}); + } else if (! darkmodeIsOn && onoff !== "off") { + document.body.classList.remove("lightmode"); + document.body.classList.add("darkmode"); + firstwindow?.postMessage({event: "darkmodeOn"}); + secondwindow?.postMessage({event: "darkmodeOn"}); + } + } +} + + +/* timeToMillisec -- convert MM:SS, DDs, DDm and DDh to milliseconds */ +function timeToMillisec(s) +{ + var m; + + if ((m = /^([0-9]+):([0-9]{2})$/.exec(s))) return 60000 * m[1] + 1000 * m[2]; + else if ((m = /^([0-9]+|[0-9]*\.[0-9]+)s$/.exec(s))) return 1000 * m[1]; + else if ((m = /^([0-9]+|[0-9]*\.[0-9]+)m$/.exec(s))) return 60000 * m[1]; + else if ((m = /^([0-9]+|[0-9]*\.[0-9]+)h$/.exec(s))) return 3600000 * m[1]; + else if ((m = /^(0+|0*\.0+)$/.exec(s))) return 0; + else return 0; +} + + +/* scrollSlide -- ensure visible incrementals are above the bottom */ +function scrollSlide() +{ + var compStyle, i, h, border, pad, bottom; + + // Get the bottom of the current slide and the bottom padding and border. + bottom = curslide.getBoundingClientRect().bottom; + compStyle = window.getComputedStyle(curslide); + border = parseFloat(compStyle.getPropertyValue("border-bottom-width")); + pad = parseFloat(compStyle.getPropertyValue("padding-bottom")); + + // Find the bottom of the visible incremental that extends the + // farthest down; or the top of the slide, if there are none. + h = bottom - scale * (curslide.scrollTop + border + pad); + for (i = 0; i <= incrementals.cur; i++) + h = Math.max(h, incrementals[i].getBoundingClientRect().bottom); + + // Scroll the current slide. + curslide.scroll(0, curslide.scrollTop + (h - bottom)/scale + border + pad); +} + + +/* nextSlideOrElt -- next incremental element or next slide if none */ +function nextSlideOrElt() +{ + var m; + + console.assert(slidemode); + + if (curslide == null) return; + + if (incrementals.cur + 1 < incrementals.length) { + /* There is a next incremental element. */ + + /* Mark the current incremental element, if any, as visited. */ + if (incrementals.cur >= 0) { + incrementals[incrementals.cur].classList.add("visited"); + incrementals[incrementals.cur].classList.remove("active"); + } + + /* Make the next one active. */ + incrementals.cur++; + incrementals[incrementals.cur].classList.add("active"); + + /* Make screen readers announce the newly displayed element */ + liveregion.innerHTML = ""; // Make it empty + liveregion.appendChild(cloneNodeWithoutID(incrementals[incrementals.cur])); + + /* In case the slide is overflowing, scroll the contents so that + * the newly displayed element is visible at the bottom of the + * slide. */ + scrollSlide(); + + /* If the element, slide or BODY has a timing attribute and it is + * not 0, set a timeout. If the element itself has a data-timing + * attribute, use that. Otherwise, if the slide has a data + * attribute, use that, divided by the number of incrementals + 1. + * Otherwise, use the default (from the BODY), divided by the + * number of incrementals + 1. */ + clearTimeout(slideTimer); + if (! slideTimerPaused && + (m = incrementals[incrementals.cur].dataset.timing !== undefined ? + timeToMillisec(incrementals[incrementals.cur].dataset.timing) : + curslide.dataset.timing !== undefined ? + timeToMillisec(curslide.dataset.timing)/(incrementals.length+1) : + slideTiming/(incrementals.length + 1))) + slideTimer = setTimeout(nextSlideOrElt, m); + + } else { + /* There is no next incremental element. So go to next slide. */ + nextSlide(); + } +} + + +/* nextSlide -- display the next slide, if any */ +function nextSlide() +{ + var h; + + console.assert(slidemode); + + if (curslide == null) return; + + console.assert(isStartOfSlide(curslide)); + + /* curslide has class=slide, page-break-before=always or is an H1 */ + h = curslide.nextSibling; + while (h && ! isStartOfSlide(h)) h = h.nextSibling; + + if (h) makeCurrent(h); // Found a next slide + else if (loopSlideShow) firstSlide(); // No next slide, but loop + else return; // No next slide + + /* The slide may have more content than fits the slide. Scroll to + * display the lowest of the visible incremental elements, or the + * top of the slide, if there are none. */ + scrollSlide(); +} + + +/* previousSlideOrElt -- next incremental element or next slide if none */ +function previousSlideOrElt() +{ + console.assert(slidemode); + + if (curslide == null) return; + + if (incrementals.cur >= 0) { + // There is an incremental element being displayed. + + // Mark the currently active element as inactive and decrement cur. + incrementals[incrementals.cur--].classList.remove("active"); + // TODO: Remove it from the liveregion. + + if (incrementalsBehavior === "forwardonly") { + // Hide all visited elements and set cur to -1. + while (incrementals.cur >= 0) + incrementals[incrementals.cur--].classList.remove("visited"); + // TODO: Remove them from the liveregion. + } else { + // If there is a preceding incremental element, make it active. + if (incrementals.cur >= 0) { + incrementals[incrementals.cur].classList.remove("visited"); + incrementals[incrementals.cur].classList.add("active"); + } + } + scrollSlide(); // In case the slide overflows, scroll + + } else { + // There is no active incremental element. Go to previous slide. + previousSlide(); + } +} + + +/* previousSlide -- display the next slide, if any */ +function previousSlide() +{ + var h; + + console.assert(slidemode); + + if (curslide == null) return; + + console.assert(isStartOfSlide(curslide)); + + h = curslide.previousSibling; + while (h && ! isStartOfSlide(h)) h = h.previousSibling; + + if (! h) return; // Found no previous slide + makeCurrent(h); + + /* The slide may have more content than fits the slide. Make sure + * the lowest of the incremental elements is above the bottom of the + * slide. Or scroll to the top of the slide if there are no + * incremental elements. */ + scrollSlide(); +} + + +/* firstSlide -- display the first slide */ +function firstSlide() +{ + var h; + + console.assert(slidemode); + + h = document.body.firstChild; + while (h && ! isStartOfSlide(h)) h = h.nextSibling; + + if (h != null) makeCurrent(h); +} + + +/* lastSlide -- display the last slide */ +function lastSlide() +{ + var h; + + console.assert(slidemode); + + h = document.body.lastChild; + while (h && ! isStartOfSlide(h)) h = h.previousSibling; + + if (h != null) makeCurrent(h); +} + + +/* findSlide -- find the slide with the ID or the number "target" */ +function findSlide(target) +{ + var h, n; + + if ((h = document.getElementById(target))) + /* Find enclosing .slide or preceding start of slide */ + while (h && ! isStartOfSlide(h)) h = h.previousSibling || h.parentNode; + else if ((n = parseInt(target)) > 0) + /* Find the start of the n'th slide. */ + for (h = document.body.firstChild; h; h = h.nextSibling) + if (h.b6slidenum === n) break; + + return h; +} + + +/* targetSlide -- display slide containing ID=target, or the target'th slide */ +function targetSlide(target) +{ + var h; + + h = findSlide(target) + /* If found, and it is not already displayed, display it */ + if (h != null) makeCurrent(h); +} + + +/* mouseButtonClick -- handle mouse click event */ +function mouseButtonClick(e) +{ + var target = e.target; + + if (noclick) return; + if (e.button != 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + if (e.detail != 1) return; // It's the 2nd of a double click + + if (syncmode) { // In sync mode, accept the click but do nothing. + + // warnSyncMode(); + + } else { + + // check if target is not something that probably wants clicks + // e.g. embed, object, input, textarea, select, option + while (target) { + if (target.nodeName === "A" || target.nodeName === "EMBED" || + target.nodeName === "OBJECT" || target.nodeName === "INPUT" || + target.nodeName === "TEXTAREA" || target.nodeName === "SELECT" || + target.nodeName === "SUMMARY" || target.nodeName === "OPTION") return; + target = target.parentNode; + } + + if (slidemode) { + // Set a timeout to handle the click after 300 ms. If a double click + // occurs in that period, it will remove the timeout and the click + // will thus not do anything. The 300 ms is a compromise. The actual + // time within which a double click occurs depends on the browser + // and the OS. 200 ms is for fast clickers, but 400 ms would cause a + // noticeable delay before the slide advances. Note that adding + // class=noclick on the body disables handling of single clicks + // completely. + singleClickTimer = setTimeout(() => {nextSlideOrElt()}, 300); + } else if (secondwindow?.closed === false) { + // Not in slide mode, but there is a 2nd window, so let it + // handle the click. + secondwindow.postMessage({event: "click"}); + } + } + + e.preventDefault(); + e.stopPropagation(); +} + + +/* gestureStart -- handle start of a touch event */ +function gestureStart(e) +{ + if (!gesture.on) { + gesture.on = true; + gesture.x2 = gesture.x1 = e.touches[0].clientX; + gesture.y2 = gesture.y1 = e.touches[0].clientY; + gesture.opacity = document.body.style.opacity; + } + gesture.touches = e.touches.length; +} + + +/* gestureMove -- handle move event */ +function gestureMove(e) +{ + if (gesture.on && slidemode) { + gesture.x2 = e.touches[0].clientX; + gesture.y2 = e.touches[0].clientY; + + /* Give some visual feedback: */ + var dx = Math.abs(gesture.x2 - gesture.x1); + var dy = Math.abs(gesture.y2 - gesture.y1); + if (gesture.touches != 1) + document.body.style.opacity = gesture.opacity; + else if (dx > dy) + document.body.style.opacity = 1 - dx / window.innerWidth; + else + document.body.style.opacity = 1 - (6 * dx - 5 * dy) / window.innerWidth; + } +} + + +/* gestureEnd -- handle end of a touch event */ +function gestureEnd(e) +{ + if (gesture.on) { + gesture.on = false; + + /* Undo visual feedback */ + if (slidemode) + document.body.style.opacity = gesture.opacity; + + var dx = gesture.x2 - gesture.x1; + var dy = gesture.y2 - gesture.y1; + + if (gesture.touches > 2) { // 3-finger gesture + if (syncmode) warnSyncMode(); + else if (slidemode) toggleModeAndFullscreen(); // Leave slide mode + else if (secondwindow?.closed !== false) toggleModeAndFullscreen(); + else secondwindow.postMessage({event: "keydown", v: "a"}); + + } else if (gesture.touches == 2) { // 2-finger gesture + return; // does nothing + + } else { // 1-finger swipe + // A swipe can mean previousSlide, nextSlide() or + // nextSlideOrElt(). The latter only if clicks are disabled + // ("noclick"). A swipe in slide mode works directly, a swipe + // while there is a 2nd window sends an event to that window, + // otherwise the swipe is ignored. + if (Math.abs(dx) < window.innerWidth/3) return; // Swipe too short + if (Math.abs(dx) < Math.abs(dy)) return; // Swipe too vertical + if (syncmode) warnSyncMode(); + else if (slidemode) { + if (dx > 0) previousSlide(); + else if (noclick) nextSlideOrElt(); + else nextSlide(); + } else if (secondwindow?.closed === false) { + if (dx > 0) secondwindow.postMessage({event: "keydown", v:"ArrowLeft"}); + else if (noclick) secondwindow.postMessage({event: "keydown", v: " "}); + else secondwindow.postMessage({event: "keydown", v: "PageDown"}); + } + } + e.preventDefault(); + e.stopPropagation(); + } +} + + +/* gestureCancel -- handle cancellation of a touch event */ +function gestureCancel(e) +{ + if (gesture.on) { + gesture.on = false; + /* Undo visual feedback */ + if (slidemode) document.body.style.opacity = gesture.opacity; + } +} + + +/* doubleClick -- handle a double click on the body */ +function doubleClick(event) +{ + var h; + + if (event.button != 0 || event.altKey || event.ctrlKey || + event.metaKey || event.shiftKey) return; + + if (!noclick) { + /* In slide mode, with the mouseButtonClick() handler installed to + * advance the slides on a single click, a double click cancels + * the effect of the single click: It removes the action that + * mouseButtonClick() had put on the queue. */ + clearTimeout(singleClickTimer); + singleClickTimer = null; + } + + /* The double click may have selected some text, so unselect everything. */ + if (! slidemode) document.getSelection().removeAllRanges(); + + /* Find on which slide, if any, the clicks occurred. */ + h = event.target; + while (h && ! isStartOfSlide(h)) h = h.previousSibling || h.parentNode; + + if (syncmode) { + warnSyncMode(); + } else if (secondwindow?.closed === false) { + // There is 2nd window. If the double click was on a slide, let + // the 2nd window move to that slide, otherwise do nothing. + if (h) secondwindow.postMessage({ event: "slide", v: h.id }); + } else if (!slidemode) { + // Enter slide mode. If the double click was on or inside a slide, + // start with that slide. + curslide = h; // May be null + toggleModeAndFullscreen(); + } + + event.preventDefault(); + event.stopPropagation(); +} + + +/* hashchange -- handle fragment id event, make target slide the current one */ +function hashchange(e) +{ + var e; + + if (!location.hash) return; + if (slidemode) + // In slidemode, try to show the targeted slide, if any. + targetSlide(location.hash.substring(1)) + else if (!commentsVisible && + (e = document.getElementById(location.hash.substring(1))?. + closest('.comment'))) { + // In index mode, if comments are currently hidden and the target + // is a comment (or inside a comment) then make make that comment + // visible again, if it wasn't already. + toggleOneComment(e, true); + e.scrollIntoView(); + } +} + + +/* message -- handle a postMessage */ +function message(e) +{ + var newEvent, h; + + if (e.source == secondwindow) { // Message from 2nd window to 1st window + + switch (e.data.event) { + case "init": // Second window has started + document.body.classList.add("has-2nd-window"); + secondwindow.postMessage({event: "startTime", v: startTime}); + secondwindow.postMessage({event: "duration", v: duration}); + secondwindow.postMessage({event: "pauseStartTime", v: pauseStartTime}); + secondwindow.postMessage({event: document.body.classList + .contains("darkmode") ? "darkmodeOn" : "darkmodeOff"}); + // Remember previous state and make speaker notes visible if + // they are currently hidden. + commentsWereVisible = commentsVisible; + if (! commentsVisible) toggleComments(); + break; + case "startTime": // Other window informs us of a new start time + startTime = e.data.v; + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + break; + case "duration": // Other window informs us of a new duration + duration = e.data.v; + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + break; + case "pauseStartTime": // Other window got a pause/resume event + pauseStartTime = e.data.v; + if (pauseStartTime) document.body.classList.add("paused"); + else document.body.classList.remove("paused"); + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + break; + case "keydown": + newEvent = new KeyboardEvent("keydown", {key: e.data.v, bubbles: true}); + document.body.dispatchEvent(newEvent); + break; + case "slide": // Make the slide with given id current + if ((h = findSlide(e.data.v))) { + if (curslide != h) curslide?.classList.remove("active"); + curslide = h; + curslide.classList.add("active"); + curslide.scrollIntoView({behavior: "smooth", block: "center"}); + history.replaceState({}, "", "#" + curslide.id) // Show in location bar + } + break; + case "noslide": // 2nd window left slide mode + curslide?.classList.remove("active"); + document.body.classList.remove("has-2nd-window"); + // Hide speaker notes if they were hidden before the 2nd window opened. + if (! commentsWereVisible && commentsVisible) toggleComments(); + // Put current slide in the URL, so the index view can highlight it. + if (curslide) location.replace("#" + curslide.id); + curslide = null; + break; + case "darkmodeOn": // Second window tells us it entered dark mode + toggleDarkMode("on"); + break; + case "darkmodeOff": // Second window tells us it left dark mode + toggleDarkMode("off"); + break; + case 'pause': // Second window tells us a video was paused + if ((h = document.getElementById(e.data.id))) { + h.b6pausing = true; + h.pause(); + } + break; + case 'play': // Second window tells us a video was started + if ((h = document.getElementById(e.data.id))) { + h.b6playing = true; + h.play(); + } + break; + case 'seeked': // Second window tells us a video was seeked + if ((h = document.getElementById(e.data.id))) { + h.b6seeking = true; + h.currentTime = e.data.v; + } + break; + case 'volumechange': // Second window tells us a video was (un)muted + if ((h = document.getElementById(e.data.id))) h.muted = e.data.v; + console.log(`volume = ${h.volume}`); + break; + } + + } else if (e.source == firstwindow) { // Message from 1st window to 2nd + + switch (e.data.event) { + case "startTime": // 1st window tells us of new start time + startTime = e.data.v; + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + break; + case "duration": // 1st window tells us of new duration + duration = e.data.v; + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + break; + case "pauseStartTime": // 1st window got a pause/resume event + pauseStartTime = e.data.v; + if (pauseStartTime) document.body.classList.add("paused"); + else document.body.classList.remove("paused"); + forceClocks = true; + requestClocksUpdate(); // Queue an update to the clocks + break; + case "click": + newEvent = new MouseEvent("click", {detail: 1, bubbles: true}); + document.body.dispatchEvent(newEvent); + break; + case "slide": // First window tells us to go to a slide + if ((h = findSlide(e.data.v))) makeCurrent(h); + break; + case "keydown": + newEvent = new KeyboardEvent("keydown", {key: e.data.v, bubbles: true}); + document.body.dispatchEvent(newEvent); + break; + case "darkmodeOn": // First window tells us it entered dark mode + toggleDarkMode("on"); + break; + case "darkmodeOff": // First window tells us it left dark mode + toggleDarkMode("off"); + break; + case "sync-on": + syncmode = true; + break; + case "sync-off": + syncmode = false; + break; + case "sync": // Navigate ("+", "-", etc, or a slide ID) + syncSlide(e.data.v); + break; + case 'pause': // First window tells us a video was paused + if ((h = document.getElementById(e.data.id))) { + h.b6pausing = true; + h.pause(); + } + break; + case 'play': // First window tells us a video started + if ((h = document.getElementById(e.data.id))) { + h.b6playing = true; + h.play(); + } + break; + case 'seeked': // First window tells us a video was seeked + if ((h = document.getElementById(e.data.id))) { + h.b6seeking = true; + h.currentTime = e.data.v; + } + break; + case 'volumechange': // First window tells us a video was (un)muted + if ((h = document.getElementById(e.data.id))) h.muted = e.data.v; + console.log(`volume = ${h.volume}`); + break; + } + } +} + + +/* windowResize -- handle a resize of the window */ +function windowResize(ev) +{ + if (slidemode) scaleBody(); // Recalculate the transform property +} + + +/* syncSlide -- handle the command string from a server-sent event */ +function syncSlide(command) +{ + if (secondwindow?.closed !== false) { + // There is no 2nd window, or we are ourselves the 2nd window. + // Just after entering slidemode, finishToggleMode() won't have + // run yet and curslide will not be set, which means next and + // previous will do nothing. But that is OK. + // The ":on" and ":off" messages are internal messages, + // generated in message(). + switch (command) { + case "+": if (!slidemode) toggleMode(); nextSlideOrElt(); break; + case "++": if (!slidemode) toggleMode(); nextSlide(); break; + case "-": if (!slidemode) toggleMode(); previousSlideOrElt(); break; + case "--": if (!slidemode) toggleMode(); previousSlide(); break; + case "^": if (!slidemode) toggleMode(); firstSlide(); break; + case "$": if (!slidemode) toggleMode(); lastSlide(); break; + case "0": if (slidemode) toggleMode(); break; + case ":dark-on": toggleDarkMode("on"); break; + case ":dark-off": toggleDarkMode("off"); break; + default: if (!slidemode) toggleMode(); targetSlide(command); // ID or # + } + } else { // There is a 2nd window + secondwindow.postMessage({event: "sync", v: command}); + } +} + + +/* syncHandler -- handle a server-sent event with a slide number or slide ID */ +function syncHandler(event) +{ + if (syncmode) syncSlide(event.data); +} + + +/* fullscreenChanged -- handle a fullscreenchange event */ +function fullscreenChanged(ev) +{ + console.assert(!firstwindow); // We assume this is the first window + + if (switchFullscreen) { + // We are entering or leaving fullscreen mode because the F1 or F + // key was pressed. Reset the flag, but don't change slide mode. + switchFullscreen = false; + } else { + // We are entering or leaving fullscreen mode, but not because the + // F1 or F keys were pressed. (Most likely, the Escape key was + // pressed while in fullscreen mode, or the user used a browser + // function to enter fullscreen mode.) If we are leaving + // fullscreen mode while in slide mode, then leave slide mode. And + // if we are entering fullscreen mode while not in slide mode, + // then enter slide mode. + if ((! document.fullscreenElement && slidemode) || + (document.fullscreenElement && !slidemode)) { + // If the help text is on screen, remove it. Do this before + // calling toggleMode(), because toggleMode() changes the style + // attribute of all toplevel elements and we don't want the + // style of the helptext to change. + if (helptext?.parentElement) helptext.remove(); + if (toctext) toctext.close(); + toggleMode(); + } + } +} + + +/* playButtonClick -- handle activation of the play/stop button */ +function playButtonClick(ev) +{ + if (syncmode) warnSyncMode(); + else if (secondwindow?.closed !== false) toggleModeAndFullscreen(); + else secondwindow.postMessage({event: "keydown", v: "a"}); + + // Avoid capturing keyboard events that are meant for navigating slides: + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* secondWindowButtonClick -- handle activation of the 2nd-window button */ +function secondWindowButtonClick(ev) +{ + console.assert(!firstwindow); // We're on the first window + + if (syncmode) warnSyncMode() + else if (secondwindow?.closed !== false) openSecondWindow() + else secondwindow.postMessage({event: "keydown", v: "a"}); + + // Avoid capturing keyboard events that are meant for navigating slides: + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* prevButtonClick -- handle activation of the prevbutton */ +function prevButtonClick(ev) +{ + // We're on the first window. + console.assert(!firstwindow); + + if (secondwindow?.closed === false) { + // There is a second window, send a left arrow key key event to it. + secondwindow.postMessage({event: "keydown", v: "ArrowLeft"}); + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); + } else { + // Start slide mode, as if play was clicked + console.assert(!slidemode); + playButtonClick(ev); + } +} + + +/* nextButtonClick -- handle activation of the nextbutton */ +function nextButtonClick(ev) +{ + // We're on the first window. + console.assert(!firstwindow); + + if (secondwindow?.closed === false) { + // There is a second window, send a space bar key event to it. + secondwindow.postMessage({event: "keydown", v: " "}); + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); + } else { + // Start slide mode, as if play was clicked + console.assert(!slidemode); + playButtonClick(ev); + } +} + + +/* darkModeButtonClick -- handle activation of the darkmodebutton */ +function darkModeButtonClick(ev) +{ + if (syncmode) warnSyncMode() + else toggleDarkMode(); + + // Avoid capturing keyboard events that are meant for navigating slides: + ev.currentTarget.blur(); + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* commentButtonClick -- handle activation of the commentbutton */ +function commentButtonClick(ev) +{ + if (syncmode) warnSyncMode(); + else requestAnimationFrame(() => toggleComments()); + + // Avoid capturing keyboard events that are meant for navigating slides: + ev.currentTarget.blur(); + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* beforeUnload -- handle a beforeunload event */ +function beforeUnload(ev) +{ + if (secondwindow?.closed === false) + // We are a firstwindow that has a second window. Close it. + secondwindow.close(); + else if (firstwindow) + // We are a second window. Tell the first window to exit slidemode. + firstwindow.postMessage({event: 'keydown', v: 'Escape'}); +} + + +/* initDarkMode -- set hasDarkMode depending on the style sheet and slides */ +function initDarkMode() +{ + // If the body element has one of the classes darkmode or lightmode, + // the author doesn't intend for any slides to change, so we set + // hasDarkMode to false. Otherwise we check if the style sheet + // supports dark mode. + // + // A style sheet that supports the classes "darkmode" and + // "lightmode" on the body element should signal that by setting the + // property "--has-darkmode" to "1" on the body element. + // + hasDarkMode = + ! document.body.classList.contains('darkmode') && + ! document.body.classList.contains('lightmode') && + window.getComputedStyle(document.body) + .getPropertyValue('--has-darkmode') == "1"; +} + + +/* addUI -- add buttons for slide mode and help */ +function addUI() +{ + var playbutton, secondwindowbutton, helpbutton, prevbutton, nextbutton, + darkmodebutton, commentbutton, div, playbuttons, secondwindowbuttons, + helpbuttons, prevbuttons, nextbuttons, darkmodebuttons, commentbuttons; + + if (firstwindow) return; // Do not add buttons on a second window + + // Wrap the buttons in a div with class "b6-ui". + // If there already are elements with that class, use the first such one. + if (! (div = document.getElementsByClassName("b6-ui")[0])) { + div = document.createElement("div"); + div.setAttribute("class", "b6-ui"); + document.body.prepend(div); // Insert the div before the slides. + } + + // Create a play button. Clicking it has the same effect as pressing + // the "A" key, i.e., enter slide mode. + // If there already are such buttons, use those. + playbuttons = document.getElementsByClassName("b6-playbutton"); + if (playbuttons.length == 0) { + playbutton = document.createElement("button"); + playbutton.innerHTML = "" + _("▶\uFE0E") + " " + + _("play/stop") + ""; + playbutton.setAttribute("class", "b6-playbutton"); + playbutton.setAttribute("title", _("play slides or stop playing")); + div.append(playbutton); + } + for (playbutton of playbuttons) { + playbutton.addEventListener("click", playButtonClick); + playbutton.addEventListener("dblclick", ignoreEvent); + } + + // Create a 2nd-window button. Clicking it has the same effect as + // pressing "2" when in slide mode, i.e., create a second window. + // If there already are such buttons, use those. + secondwindowbuttons = + document.getElementsByClassName("b6-secondwindowbutton"); + if (secondwindowbuttons.length == 0) { + secondwindowbutton = document.createElement("button"); + secondwindowbutton.innerHTML = "" + _("⧉") + " " + + _("play in 2nd window") + ""; + secondwindowbutton.setAttribute("class", "b6-secondwindowbutton"); + secondwindowbutton.setAttribute("title", + _("play/stop slides in a 2nd window")); + div.append(secondwindowbutton); + } + for (secondwindowbutton of secondwindowbuttons) { + secondwindowbutton.addEventListener("click", secondWindowButtonClick); + secondwindowbutton.addEventListener("dblclick", ignoreEvent); + } + + /* Create buttons for next and previous. */ + // If there already are such buttons, use those. + prevbuttons = document.getElementsByClassName("b6-prevbutton"); + if (prevbuttons.length == 0) { + prevbutton = document.createElement("button"); + prevbutton.innerHTML = "" + _("❮") + " " + + _("back") + ""; + prevbutton.setAttribute("class", "b6-prevbutton"); + prevbutton.setAttribute("title", _("previous slide")) + div.append(prevbutton); + } + for (prevbutton of prevbuttons) { + prevbutton.addEventListener("click", prevButtonClick); + prevbutton.addEventListener("dblclick", ignoreEvent); + } + + nextbuttons = document.getElementsByClassName("b6-nextbutton"); + if (nextbuttons.length == 0) { + nextbutton = document.createElement("button"); + nextbutton.innerHTML = "" + _("❯") + " " + + _("forward") + ""; + nextbutton.setAttribute("class", "b6-nextbutton"); + nextbutton.setAttribute("title", _("next slide or element")) + div.append(nextbutton); + } + for (nextbutton of nextbuttons) { + nextbutton.addEventListener("click", nextButtonClick); + nextbutton.addEventListener("dblclick", ignoreEvent); + } + + // Create a dark mode toggle, if the style sheet has support for + // dark mode. Clicking it has the same effect as pressing "d" when + // in slide mode, i.e., add or remove class=darkmode on BODY. + if (hasDarkMode) { + darkmodebuttons = document.getElementsByClassName("b6-darkmodebutton"); + if (darkmodebuttons.length == 0) { + darkmodebutton = document.createElement("button"); + darkmodebutton.innerHTML = "" + _("◑") + " " + + _("dark mode") + ""; + darkmodebutton.setAttribute("class", "b6-darkmodebutton"); + darkmodebutton.setAttribute("title", _("toggle dark mode on/off")); + div.append(darkmodebutton); + } + for (darkmodebutton of darkmodebuttons) { + darkmodebutton.addEventListener("click", darkModeButtonClick); + darkmodebutton.addEventListener("dblclick", ignoreEvent); + } + } + + // Create a button to toggle the display of comments. + commentbuttons = document.getElementsByClassName("b6-commentbutton"); + if (commentbuttons.length == 0) { + commentbutton = document.createElement("button"); + commentbutton.innerHTML = "" + _("🗊") + " " + + _("notes") + ""; + commentbutton.setAttribute("class", "b6-commentbutton"); + commentbutton.setAttribute("title", _("show/hide notes")); + div.append(commentbutton); + } + for (commentbutton of commentbuttons) { + commentbutton.addEventListener("click", commentButtonClick); + commentbutton.addEventListener("dblclick", ignoreEvent); + } + + // Create a help button. Clicking it has the same effect as pressing + // "?" when in slide mode, i.e., pop up the help window. + // If there already is such a button, use that. + helpbuttons = document.getElementsByClassName("b6-helpbutton"); + if (helpbuttons.length == 0) { + helpbutton = document.createElement("button"); + helpbutton.innerHTML = "" + _("?") + " " + _("help") + + ""; + helpbutton.setAttribute("class", "b6-helpbutton"); + helpbutton.setAttribute("title", _("help")); + div.append(helpbutton); + } + for (helpbutton of helpbuttons) { + helpbutton.addEventListener("click", ev => { + help(); + ev.currentTarget.blur(); + ev.preventDefault(); + ev.stopPropagation(); + }); + helpbutton.addEventListener("dblclick", ignoreEvent); + } + + // // Add logo of b6+ with a link to its home page. + // div.insertAdjacentHTML("beforeend", + // "" + + // ""); + +} + + +/* checkURL -- process query parameters ("full", "static" and "sync") */ +function checkURL() +{ + var h; + + const params = new URLSearchParams(location.search); + if (params.get("full") != null) fullmode = true; + if (params.get("static") != null) interactive = false; + if ((h = params.get("visible-notes")) != null) commentsDefault = h !== 'off'; + if ((syncURL = params.get("sync"))) tryToggleSync(); +} + + +/* checkIfFramed -- if we're inside an iframe, add target=_parent to links */ +function checkIfFramed() +{ + var anchors, i; + + if (window.parent != window) { // Only if we're not the top document + anchors = document.getElementsByTagName('a'); + for (i = 0; i < anchors.length; i++) + if (!anchors[i].hasAttribute('target')) + anchors[i].setAttribute('target', '_parent'); + document.body.classList.add('framed'); // Allow the style to do things + } +} + + +/* checkOptions -- look for b6plus options in the class attribute on body */ +function checkOptions() +{ + var c, t; + + for (c of document.body.classList) + if (c === 'noclick') { + noclick = 1; + } else if ((t = c.match(/^hidemouse(=([0-9.]+))?$/))) { + hideMouseTime = 1000 * (t[2] ?? 5); // Default is 5s if no time given + } else if ((t = c.match(/^incremental-([a-z]+)$/))) { + if (t[1] !== "freeze" && t[1] !== "reset" && t[1] !== "forwardonly" && + t[1] !== "symmetric") + console.warn(`"${t[1]}" is not a valid value after "incremental=". Must be one of "symmetric", "reset", "forwardonly" or "freeze". Falling back to "${incrementalsBehavior}".`); + else + incrementalsBehavior = t[1]; + } else if (c === 'loop') { + loopSlideShow = true; + } else if (c === 'visible-notes') { + commentsDefault = true; + } + + /* Default time for automatically advancing slides. 0 means don't advance. */ + slideTiming = timeToMillisec(document.body.dataset.timing); +} + + +/* initLanguage -- determine the language to localize to */ +function initLanguage() +{ + var i; + + // Get language from the HTML element, default to en-us + language = (document.documentElement.getAttribute("lang") ?? "en-us") + .toLowerCase(); + + // Remove subtags until we have a match among the translations of "min" + while (!translations["min"][language] && (i = language.lastIndexOf("-")) >= 0) + language = language.slice(0, i); +} + + +/* checkIfSecondWindow -- if this is a second windo, configure it */ +function checkIfSecondWindow() +{ + var styleElt; + + if (window.opener && window.opener != window) { + + // If this is a second window, remember the corresponding first + // one, so we can use postMessage() on it. We need to store it in + // a variable, because after the next open("#foo"), window.opener + // will be reset to window. + firstwindow = window.opener; + + // Modify the title. + document.title = "Drag to 2nd screen, then press F for full screen  |  " + + document.title + "  |  b6+"; + + // Accessibility hint. + if (interactive) document.body.setAttribute("role", "application"); + + // Add a message handler for messages from the first window. + window.addEventListener("message", message); + + // Go into slide mode. Once in slide mode, finishToggleMode() will + // send an "init" event to the first window. + toggleMode(); + } +} + + +/* recordMousePosition -- actions when the mouse is over a slide */ +function recordMousePosition(ev) +{ + mouseX = ev.clientX; + mouseY = ev.clientY; +} + + +/* showURL -- show a popover with the URL of the slide under the mouse */ +function showURL() +{ + // This shows the URL of the slide that the mouse is over, but only + // when the Alt key is pressed and only in index mode. + // + var url, button, span, h, rect; + + console.assert(!slidemode); + + // Create an overlay if not created yet. + if (! hoverOverlay) { + hoverOverlay = document.createElement('div'); + button = document.createElement('button'); + button.textContent = _('Copy URL'); + button.style.marginRight = '1em'; + button.addEventListener('click', + (f) => navigator.clipboard.writeText(f.currentTarget.value)); + span = document.createElement('span'); + span.style.wordBreak = 'break-all'; + hoverOverlay.append(button, span); + hoverOverlay.style.margin = '0 -100% 0 0'; + hoverOverlay.style.maxWidth = '90%'; + hoverOverlay.style.position = 'absolute'; + hoverOverlay.popover = 'auto'; + document.body.append(hoverOverlay); + } + + h = document.elementFromPoint(mouseX, mouseY); + while (h && ! isStartOfSlide(h)) h = h.previousSibling || h.parentNode; + if (h) { + url = new URL(location); + url.hash = h.id; + hoverOverlay.firstChild.value = + hoverOverlay.firstChild.nextSibling.textContent = url.toString(); + + requestAnimationFrame(() => { + // First pop it up in the top left corner. + hoverOverlay.style.top = '0'; + hoverOverlay.style.left = '0'; + hoverOverlay.showPopover({source: h}); + + // Now move the overlay as near as possible to the mouse position + // without extending outside the viewport. + rect = hoverOverlay.getBoundingClientRect(); + hoverOverlay.style.left = (visualViewport.pageLeft + + Math.max(0, Math.min(visualViewport.width - + rect.width, mouseX - 2))) + 'px'; + hoverOverlay.style.top = (visualViewport.pageTop + mouseY - 2) + 'px'; + }); + } +} + + +/* toggleOneComment -- make a comment element visible or invisible */ +function toggleOneComment(h, makeVisible) +{ + if (makeVisible) { + console.assert(h.b6savedstyle !== undefined); + h.style.cssText = h.b6savedstyle; + } else { + if (h.b6savedstyle === undefined) h.b6savedstyle = h.style.cssText; + h.style.visibility = "hidden"; + h.style.position = "absolute"; + h.style.overflow = "hidden"; + h.style.top = h.style.left = h.style.width = h.style.height = "0"; + } +} + + +/* toggleComments -- hide or show comments (speaker notes) after slides */ +function toggleComments() +{ + var e, after = false; + var oldrect, newrect; + + // Get the position in the viewport of one of the slides that is + // visible in the viewport, so we can scroll the page to the same + // slide after we hide or show the comments. Either use the same + // slide we used last time we were here, if it is still visible, or + // the first slide that is visible. + if (!visibleSlide?.b6IsInViewport) { + visibleSlide = document.body.firstChild; + while (visibleSlide && !visibleSlide.b6IsInViewport) + visibleSlide = visibleSlide.nextSibling; + } + oldrect = visibleSlide?.getBoundingClientRect(); + + // Hide or show all comments, except any that occur before the first slide. + if (commentsVisible) { // Comments are currently visible + for (const h of document.body.children) { + if (isStartOfSlide(h)) + after = true; + else if (after && h.classList.contains('comment')) + toggleOneComment(h, false) + } + commentsVisible = false; + } else { // Comments are currently hidden. + for (const h of document.body.children) { + if (isStartOfSlide(h)) + after = true; + else if (after && h.nodeType === 1 && h.classList.contains('comment')) + toggleOneComment(h, true) + } + commentsVisible = true; + } + + // If the current target is a comment (or inside a comment), make + // sure that comment is visible. + if (! commentsVisible && + (e = document.getElementById(location.hash.substring(1)) + ?.closest('.comment'))) + toggleOneComment(e, true); + + // Scroll such that the same slide is at the same position in the + // viewport. + visibleSlide?.scrollIntoView({block: "center"}); + newrect = visibleSlide?.getBoundingClientRect(); + if (oldrect && newrect) + scroll(visualViewport.pageLeft - oldrect.left + newrect.left, + visualViewport.pageTop - oldrect.top + newrect.top); +} + + +/* trackVisibleSlides -- keep track of which slides are in the viewport */ +function trackVisibleSlides() +{ + var observer; + + // Slides that are visible have b6IsInViewport set to true. This is + // used in toggleComments() to scroll the document to approximately + // the same slide after comments were made visible or invisible. + observer = new IntersectionObserver((entries, observer) => { + for (const e of entries) + if (e.isIntersecting) e.target.b6IsInViewport = true + else e.target.b6IsInViewport = false}); + + for (const h of document.body.children) + if (isStartOfSlide(h)) observer.observe(h); +} + + +/* initLiveRegion -- find or create an ARIA live region for announcing slides */ +function initLiveRegion() +{ + /* Find or create an element to announce the slides in speech. */ + if ((liveregion = + document.querySelector("[role=region][aria-live=assertive]"))) { + savedContent = liveregion.innerHTML; // Remember its content, if any + liveregion.innerHTML = ""; + } else { + liveregion = document.createElement("div"); + liveregion.setAttribute("role", "region"); + liveregion.setAttribute("aria-live", "assertive"); + document.body.appendChild(liveregion); + savedContent = _("Stopped."); // Default to an English message + } +} + + +/* initialize -- add event handlers, initialize state */ +function initialize() +{ + initLanguage(); // Determine the language for localized text + checkIfFramed(); // Add target attributes if needed + checkOptions(); // Look for options in body.classList + checkURL(); // Parse query parameters (full, static) + initLiveRegion(); // Find or create an ARIA live region + numberSlides(); // Count & number the slides and give them IDs + instrumentVideos(); // Add event handlers to any video elements + document.body.classList.add('b6plus'); // Tell style sheet that b6+ is used + checkIfSecondWindow(); // If this is a secondwindow, configure it + window.addEventListener('resize', windowResize, true); + + if (interactive) { // Only add event listeners if not static + initClocks(); // Find and initialize clock elements + initDarkMode(); // Set hasDarkMode to true or false + addUI(); // Add buttons for slide mode and help + trackVisibleSlides(); // Install an IntersectionObserver + if (! commentsDefault) toggleComments(); // Hide elts with class=comment + document.addEventListener('click', mouseButtonClick, false); + document.addEventListener('keydown', keyDown, true); + document.addEventListener('dblclick', doubleClick, false); + window.addEventListener('hashchange', hashchange, false); + document.addEventListener('touchstart', gestureStart, false); + document.addEventListener('touchmove', gestureMove, false); + document.addEventListener('touchend', gestureEnd, false); + document.addEventListener('touchcancel', gestureCancel, false); + window.addEventListener("message", message, false); + window.addEventListener("beforeunload", beforeUnload, false); + document.addEventListener('mousemove', recordMousePosition); + if (!firstwindow) + document.addEventListener("fullscreenchange", fullscreenChanged, false); + } + + if (fullmode) toggleMode(); // Slide mode, but not fullscreen +} + + +/* initializeLayout -- initialization that needs all resources to be loaded */ +function initializeLayout() +{ + autosize(); // Maybe shrink images with class=autosize + textFit(); // Maybe shrink font on slides w/ class=textfit +} + + +/* main */ +if (!!document.b6IsLoaded) return; // Don't load b6plus twice +document.b6IsLoaded = true; + +if (document.readyState !== 'loading') initialize(); +else document.addEventListener('DOMContentLoaded', initialize); + +if (document.readyState === 'complete') initializeLayout(); +else window.addEventListener('load', initializeLayout); + +})(); diff --git a/presentations/tpac-2025/Templates/cover-dark.svg b/presentations/tpac-2025/Templates/cover-dark.svg new file mode 100644 index 0000000..9e7f9be --- /dev/null +++ b/presentations/tpac-2025/Templates/cover-dark.svg @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentations/tpac-2025/Templates/cover.svg b/presentations/tpac-2025/Templates/cover.svg new file mode 100644 index 0000000..3c5d4f9 --- /dev/null +++ b/presentations/tpac-2025/Templates/cover.svg @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentations/tpac-2025/Templates/gutenberg_f9f9f9.jpg b/presentations/tpac-2025/Templates/gutenberg_f9f9f9.jpg new file mode 100644 index 0000000..4d5c892 Binary files /dev/null and b/presentations/tpac-2025/Templates/gutenberg_f9f9f9.jpg differ diff --git a/presentations/tpac-2025/Templates/iframe-fixup.js b/presentations/tpac-2025/Templates/iframe-fixup.js new file mode 100644 index 0000000..ddc2d51 --- /dev/null +++ b/presentations/tpac-2025/Templates/iframe-fixup.js @@ -0,0 +1,64 @@ +// This script checks if the document is displayed inside an iframe or +// similar and if so: +// +// * adds target=_parent to links (unless they already have a +// target attribute), so the links replace the parent instead of +// opening inside the iframe, and +// +// * adds class=framed to the body element, so that the style sheet +// can apply suitable styles, if needed. +// +// This is useful, e.g., in HTML slides that use the Shower script +// (and probably other scripts, too) to allow the slides to be +// displayed inside iframe elements in another document. Add the +// script to the slides with: +// +// +// +// and then include a slide in an iframe with, e.g.: +// +// +// +// The "?full" at the end of the URL tells Shower, b6+ and similar +// slide frameworks to display a single slide; and the "#cover" tells +// them to display the slide that has id=cover. +// +// This script is not necessary with the b6+ slide framework, which +// already includes equivalent code. (But it is not harmful either.) +// +// Created: 19 December 2020 +// Author: Bert Bos + + +(function() { + "use strict"; + + + // checkIfFramed -- apply some fixes if we are inside an iframe + function checkIfFramed() + { + var anchors, i; + + // Check that we're not the top document and not yet marked. + if (window.parent != window && + !document.body.classList.contains('framed')) { + + // Add target=_parent to all hyperlinks that do not have a target. + anchors = document.getElementsByTagName('a'); + for (i = 0; i < anchors.length; i++) + if (!anchors[i].hasAttribute('target')) + anchors[i].setAttribute('target', '_parent'); + + // Add a class to allow the style to do things. + document.body.classList.add('framed'); + } + } + + + // Do it if the document has been loaded, otherwise as soon as it has been. + if (document.readyState !== 'loading') checkIfFramed(); + else document.addEventListener('DOMContentLoaded', checkIfFramed); + +})(); diff --git a/presentations/tpac-2025/Templates/linen.png b/presentations/tpac-2025/Templates/linen.png new file mode 100644 index 0000000..46474bf Binary files /dev/null and b/presentations/tpac-2025/Templates/linen.png differ diff --git a/presentations/tpac-2025/Templates/piechart.png b/presentations/tpac-2025/Templates/piechart.png new file mode 100644 index 0000000..a6e884e Binary files /dev/null and b/presentations/tpac-2025/Templates/piechart.png differ diff --git a/presentations/tpac-2025/Templates/shower.js b/presentations/tpac-2025/Templates/shower.js new file mode 100644 index 0000000..15d916b --- /dev/null +++ b/presentations/tpac-2025/Templates/shower.js @@ -0,0 +1,836 @@ +/** + * Core for Shower HTML presentation engine + * @shower/core v3.2.0, https://github.com/shower/core + * @copyright 2010–2021 Vadim Makeev, https://pepelsbey.net + * @license MIT + */ +(function () { + 'use strict'; + + const isInteractiveElement = (element) => element.tabIndex !== -1; + + const contentLoaded = (callback) => { + if (document.currentScript.async) { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } + }; + + const defineReadOnly = (target, props) => { + for (const [key, value] of Object.entries(props)) { + Object.defineProperty(target, key, { + value, + writable: false, + enumerable: true, + configurable: true, + }); + } + }; + + class ShowerError extends Error {} + + var defaultOptions = { + containerSelector: '.shower', + progressSelector: '.progress', + stepSelector: '.next', + fullModeClass: 'full', + listModeClass: 'list', + mouseHiddenClass: 'pointless', + mouseInactivityTimeout: 5000, + + slideSelector: '.slide', + slideTitleSelector: 'h2', + activeSlideClass: 'active', + visitedSlideClass: 'visited', + }; + + class Slide extends EventTarget { + /** + * @param {Shower} shower + * @param {HTMLElement} element + */ + constructor(shower, element) { + super(); + + defineReadOnly(this, { + shower, + element, + state: { + visitCount: 0, + innerStepCount: 0, + }, + }); + + this._isActive = false; + this._options = this.shower.options; + + this.element.addEventListener('click', (event) => { + if (event.defaultPrevented) return; + + this.activate(); + this.shower.enterFullMode(); + }); + } + + get isActive() { + return this._isActive; + } + + get isVisited() { + return this.state.visitCount > 0; + } + + get id() { + return this.element.id; + } + + get title() { + const titleElement = this.element.querySelector(this._options.slideTitleSelector); + return titleElement ? titleElement.innerText : ''; + } + + /** + * Deactivates currently active slide (if any) and activates itself. + * @emits Slide#deactivate + * @emits Slide#activate + * @emits Shower#slidechange + */ + activate() { + if (this._isActive) return; + + const prev = this.shower.activeSlide; + if (prev) { + prev._deactivate(); + } + + this.state.visitCount++; + this.element.classList.add(this._options.activeSlideClass); + + this._isActive = true; + this.dispatchEvent(new Event('activate')); + this.shower.dispatchEvent( + new CustomEvent('slidechange', { + detail: { prev }, + }), + ); + } + + /** + * @throws {ShowerError} + * @emits Slide#deactivate + */ + deactivate() { + if (this.shower.isFullMode) { + throw new ShowerError('In full mode, another slide should be activated instead.'); + } + + if (this._isActive) { + this._deactivate(); + } + } + + _deactivate() { + this.element.classList.replace( + this._options.activeSlideClass, + this._options.visitedSlideClass, + ); + + this._isActive = false; + this.dispatchEvent(new Event('deactivate')); + } + } + + const createLiveRegion = () => { + const liveRegion = document.createElement('section'); + liveRegion.className = 'region'; + liveRegion.setAttribute('role', 'region'); + liveRegion.setAttribute('aria-live', 'assertive'); + liveRegion.setAttribute('aria-relevant', 'all'); + liveRegion.setAttribute('aria-label', 'Slide Content: Auto-updating'); + return liveRegion; + }; + + var a11y = (shower) => { + const { container } = shower; + const liveRegion = createLiveRegion(); + container.appendChild(liveRegion); + + const updateDocumentRole = () => { + if (shower.isFullMode) { + container.setAttribute('role', 'application'); + } else { + container.removeAttribute('role'); + } + }; + + const updateLiveRegion = () => { + const slide = shower.activeSlide; + if (slide) { + liveRegion.innerHTML = slide.element.innerHTML; + } + }; + + shower.addEventListener('start', () => { + updateDocumentRole(); + updateLiveRegion(); + }); + + shower.addEventListener('modechange', updateDocumentRole); + shower.addEventListener('slidechange', updateLiveRegion); + }; + + var keys = (shower) => { + const doSlideActions = (event) => { + const isShowerAction = !(event.ctrlKey || event.altKey || event.metaKey); + + switch (event.key.toUpperCase()) { + case 'ENTER': + if (event.metaKey && shower.isListMode) { + if (event.shiftKey) { + event.preventDefault(); + shower.first(); + } + + break; + } + + event.preventDefault(); + if (event.shiftKey) { + shower.prev(); + } else { + shower.next(); + } + break; + + case 'BACKSPACE': + case 'PAGEUP': + case 'ARROWUP': + case 'ARROWLEFT': + case 'H': + case 'K': + case 'P': + if (isShowerAction) { + event.preventDefault(); + shower.prev(event.shiftKey); + } + break; + + case 'PAGEDOWN': + case 'ARROWDOWN': + case 'ARROWRIGHT': + case 'L': + case 'J': + case 'N': + if (isShowerAction) { + event.preventDefault(); + shower.next(event.shiftKey); + } + break; + + case ' ': + if (isShowerAction && shower.isFullMode) { + event.preventDefault(); + if (event.shiftKey) { + shower.prev(); + } else { + shower.next(); + } + } + break; + + case 'HOME': + event.preventDefault(); + shower.first(); + break; + + case 'END': + event.preventDefault(); + shower.last(); + break; + } + }; + + const doModeActions = (event) => { + switch (event.key.toUpperCase()) { + case 'ESCAPE': + if (shower.isFullMode) { + event.preventDefault(); + shower.exitFullMode(); + } + break; + + case 'ENTER': + if (event.metaKey && shower.isListMode) { + event.preventDefault(); + shower.enterFullMode(); + } + break; + + case 'P': + if (event.metaKey && event.altKey && shower.isListMode) { + event.preventDefault(); + shower.enterFullMode(); + } + break; + + case 'F5': + if (event.shiftKey && shower.isListMode) { + event.preventDefault(); + shower.enterFullMode(); + } + break; + } + }; + + shower.container.addEventListener('keydown', (event) => { + if (event.defaultPrevented) return; + if (isInteractiveElement(event.target)) return; + + doSlideActions(event); + doModeActions(event); + }); + }; + + var location$1 = (shower) => { + const composeURL = () => { + const search = shower.isFullMode ? '?full' : ''; + const slide = shower.activeSlide; + const hash = slide ? `#${slide.id}` : ''; + + return location.pathname + search + hash; // path is required to clear search params + }; + + const applyURLMode = () => { + const isFull = new URLSearchParams(location.search).has('full'); + if (isFull) { + shower.enterFullMode(); + } else { + shower.exitFullMode(); + } + }; + + const applyURLSlide = () => { + const id = location.hash.slice(1); + if (!id) return; + + const target = shower.slides.find((slide) => slide.id === id); + if (target) { + target.activate(); + } else if (!shower.activeSlide) { + shower.first(); // invalid hash + } + }; + + const applyURL = () => { + applyURLMode(); + applyURLSlide(); + }; + + applyURL(); + window.addEventListener('popstate', applyURL); + + shower.addEventListener('start', () => { + history.replaceState(null, document.title, composeURL()); + }); + + shower.addEventListener('modechange', () => { + history.replaceState(null, document.title, composeURL()); + }); + + shower.addEventListener('slidechange', () => { + const url = composeURL(); + if (!location.href.endsWith(url)) { + history.pushState(null, document.title, url); + } + }); + }; + + var next = (shower) => { + const { stepSelector, activeSlideClass, visitedSlideClass } = shower.options; + + let innerSteps; + let activeIndex; + + const isActive = (step) => step.classList.contains(activeSlideClass); + const isVisited = (step) => step.classList.contains(visitedSlideClass); + + const setInnerStepsState = () => { + if (shower.isListMode) return; + + const slide = shower.activeSlide; + + innerSteps = [...slide.element.querySelectorAll(stepSelector)]; + activeIndex = + innerSteps.length && innerSteps.every(isVisited) + ? innerSteps.length + : innerSteps.filter(isActive).length - 1; + + slide.state.innerStepCount = innerSteps.length; + }; + + shower.addEventListener('start', setInnerStepsState); + shower.addEventListener('modechange', setInnerStepsState); + shower.addEventListener('slidechange', setInnerStepsState); + + shower.addEventListener('next', (event) => { + if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; + + activeIndex++; + innerSteps.forEach((step, index) => { + step.classList.toggle(visitedSlideClass, index < activeIndex); + step.classList.toggle(activeSlideClass, index === activeIndex); + }); + + if (activeIndex < innerSteps.length) { + event.preventDefault(); + } + }); + + shower.addEventListener('prev', (event) => { + if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; + if (activeIndex === -1 || activeIndex === innerSteps.length) return; + + activeIndex--; + innerSteps.forEach((step, index) => { + step.classList.toggle(visitedSlideClass, index < activeIndex + 1); + step.classList.toggle(activeSlideClass, index === activeIndex); + }); + + event.preventDefault(); + }); + }; + + var progress = (shower) => { + const { progressSelector } = shower.options; + const bar = shower.container.querySelector(progressSelector); + if (!bar) return; + + bar.setAttribute('role', 'progressbar'); + bar.setAttribute('aria-valuemin', 0); + bar.setAttribute('aria-valuemax', 100); + + const updateProgress = () => { + const index = shower.activeSlideIndex; + const { length } = shower.slides; + const progress = (index / (length - 1)) * 100; + + bar.style.width = `${progress}%`; + bar.setAttribute('aria-valuenow', progress); + bar.setAttribute('aria-valuetext', `Slideshow progress: ${progress}%`); + }; + + shower.addEventListener('start', updateProgress); + shower.addEventListener('slidechange', updateProgress); + }; + + const units = ['s', 'm', 'h']; + const hasUnits = (timing) => { + return units.some((unit) => timing.includes(unit)); + }; + + const parseUnits = (timing) => { + return units.map((unit) => timing.match(`(\\S+)${unit}`)).map((match) => match && match[1]); + }; + + const parseColons = (timing) => { + return `::${timing}`.split(':').reverse(); + }; + + const SEC_IN_MIN = 60; + const SEC_IN_HOUR = SEC_IN_MIN * 60; + + var parseTiming = (timing) => { + if (!timing) return 0; + + const parsed = hasUnits(timing) ? parseUnits(timing) : parseColons(timing); + + let [sec, min, hour] = parsed.map(Number); + + sec += min * SEC_IN_MIN; + sec += hour * SEC_IN_HOUR; + + return Math.max(sec * 1000, 0); + }; + + var timer = (shower) => { + let id; + + const resetTimer = () => { + clearTimeout(id); + if (shower.isListMode) return; + + const slide = shower.activeSlide; + const { visitCount, innerStepCount } = slide.state; + if (visitCount > 1) return; + + const timing = parseTiming(slide.element.dataset.timing); + if (!timing) return; + + if (innerStepCount) { + const stepTiming = timing / (innerStepCount + 1); + id = setInterval(() => shower.next(), stepTiming); + } else { + id = setTimeout(() => shower.next(), timing); + } + }; + + shower.addEventListener('start', resetTimer); + shower.addEventListener('modechange', resetTimer); + shower.addEventListener('slidechange', resetTimer); + + shower.container.addEventListener('keydown', (event) => { + if (!event.defaultPrevented) { + clearTimeout(id); + } + }); + }; + + const mdash = '\u2014'; + + var title = (shower) => { + const { title } = document; + const updateTitle = () => { + if (shower.isFullMode) { + const slide = shower.activeSlide; + const slideTitle = slide.title; + if (slideTitle) { + document.title = `${slideTitle} ${mdash} ${title}`; + return; + } + } + + document.title = title; + }; + + shower.addEventListener('start', updateTitle); + shower.addEventListener('modechange', updateTitle); + shower.addEventListener('slidechange', updateTitle); + }; + + var view = (shower) => { + const { container } = shower; + const { fullModeClass, listModeClass } = shower.options; + + if (container.classList.contains(fullModeClass)) { + shower.enterFullMode(); + } else { + container.classList.add(listModeClass); + } + + const updateScale = () => { + const firstSlide = shower.slides[0]; + if (!firstSlide) return; + + const { innerWidth, innerHeight } = window; + const { offsetWidth, offsetHeight } = firstSlide.element; + + const listScale = 1 / (offsetWidth / innerWidth); + const fullScale = 1 / Math.max(offsetWidth / innerWidth, offsetHeight / innerHeight); + + container.style.setProperty('--shower-list-scale', listScale); + container.style.setProperty('--shower-full-scale', fullScale); + }; + + const updateModeView = () => { + if (shower.isFullMode) { + container.classList.remove(listModeClass); + container.classList.add(fullModeClass); + } else { + container.classList.remove(fullModeClass); + container.classList.add(listModeClass); + } + + updateScale(); + + if (shower.isFullMode) return; + + const slide = shower.activeSlide; + if (slide) { + slide.element.scrollIntoView({ block: 'center' }); + } + }; + + shower.addEventListener('start', updateModeView); + shower.addEventListener('modechange', updateModeView); + shower.addEventListener('slidechange', () => { + if (shower.isFullMode) return; + + const slide = shower.activeSlide; + slide.element.scrollIntoView({ block: 'nearest' }); + }); + + window.addEventListener('resize', updateScale); + }; + + var touch = (shower) => { + let exitFullScreen = false; + let clickable = false; + + document.addEventListener('touchstart', (event) => { + if (event.touches.length === 1) { + const touch = event.touches[0]; + const x = touch.clientX; + const { target } = touch; + clickable = target.tabIndex !== -1; + if (!clickable) { + if (shower.isFullMode) { + if (event.cancelable) event.preventDefault(); + if (window.innerWidth / 2 < x) { + shower.next(); + } else { + shower.prev(); + } + } + } + } else if (event.touches.length === 3) { + exitFullScreen = true; + } + }); + + shower.container.addEventListener('touchend', (event) => { + if (exitFullScreen) { + event.preventDefault(); + exitFullScreen = false; + shower.exitFullMode(); + } else if (event.touches.length === 1 && !clickable && shower.isFullMode) + event.preventDefault(); + }); + }; + + var mouse = (shower) => { + const { mouseHiddenClass, mouseInactivityTimeout } = shower.options; + + let hideMouseTimeoutId = null; + + const cleanUp = () => { + shower.container.classList.remove(mouseHiddenClass); + clearTimeout(hideMouseTimeoutId); + hideMouseTimeoutId = null; + }; + + const hideMouseIfInactive = () => { + if (hideMouseTimeoutId !== null) { + cleanUp(); + } + + hideMouseTimeoutId = setTimeout(() => { + shower.container.classList.add(mouseHiddenClass); + }, mouseInactivityTimeout); + }; + + const initHideMouseIfInactiveModule = () => { + shower.container.addEventListener('mousemove', hideMouseIfInactive); + }; + + const destroyHideMouseIfInactiveModule = () => { + shower.container.removeEventListener('mousemove', hideMouseIfInactive); + cleanUp(); + }; + + const handleModeChange = () => { + if (shower.isFullMode) { + initHideMouseIfInactiveModule(); + } else { + destroyHideMouseIfInactiveModule(); + } + }; + + shower.addEventListener('start', handleModeChange); + shower.addEventListener('modechange', handleModeChange); + }; + + var installModules = (shower) => { + a11y(shower); + progress(shower); + keys(shower); + next(shower); + timer(shower); // should come after `keys` and `next` + title(shower); + location$1(shower); // should come after `title` + view(shower); + touch(shower); + mouse(shower); + + // maintains invariant: active slide always exists in `full` mode + if (shower.isFullMode && !shower.activeSlide) { + shower.first(); + } + }; + + class Shower extends EventTarget { + /** + * @param {object=} options + */ + constructor(options) { + super(); + + defineReadOnly(this, { + options: { ...defaultOptions, ...options }, + }); + + this._mode = 'list'; + this._isStarted = false; + this._container = null; + } + + /** + * @param {object=} options + * @throws {ShowerError} + */ + configure(options) { + if (this._isStarted) { + throw new ShowerError('Shower should be configured before it is started.'); + } + + Object.assign(this.options, options); + } + + /** + * @throws {ShowerError} + * @emits Shower#start + */ + start() { + if (this._isStarted) return; + + const { containerSelector } = this.options; + this._container = document.querySelector(containerSelector); + if (!this._container) { + throw new ShowerError( + `Shower container with selector '${containerSelector}' was not found.`, + ); + } + + this._initSlides(); + installModules(this); + + this._isStarted = true; + this.dispatchEvent(new Event('start')); + } + + _initSlides() { + const visibleSlideSelector = `${this.options.slideSelector}:not([hidden])`; + const visibleSlideElements = this._container.querySelectorAll(visibleSlideSelector); + + this.slides = Array.from(visibleSlideElements, (slideElement, index) => { + if (!slideElement.id) { + slideElement.id = index + 1; + } + + return new Slide(this, slideElement); + }); + } + + _setMode(mode) { + if (mode === this._mode) return; + + this._mode = mode; + this.dispatchEvent(new Event('modechange')); + } + + /** + * @param {Event} event + */ + dispatchEvent(event) { + if (!this._isStarted) return false; + + return super.dispatchEvent(event); + } + + get container() { + return this._container; + } + + get isFullMode() { + return this._mode === 'full'; + } + + get isListMode() { + return this._mode === 'list'; + } + + get activeSlide() { + return this.slides.find((slide) => slide.isActive); + } + + get activeSlideIndex() { + return this.slides.findIndex((slide) => slide.isActive); + } + + /** + * Slide fills the maximum area. + * @emits Shower#modechange + */ + enterFullMode() { + this._setMode('full'); + } + + /** + * Shower returns into list mode. + * @emits Shower#modechange + */ + exitFullMode() { + this._setMode('list'); + } + + /** + * @param {number} index + */ + goTo(index) { + const slide = this.slides[index]; + if (slide) { + slide.activate(); + } + } + + /** + * @param {number} delta + */ + goBy(delta) { + this.goTo(this.activeSlideIndex + delta); + } + + /** + * @param {boolean} [isForce=false] + * @emits Shower#prev + */ + prev(isForce) { + const prev = new Event('prev', { cancelable: !isForce }); + if (this.dispatchEvent(prev)) { + this.goBy(-1); + } + } + + /** + * @param {boolean} [isForce=false] + * @emits Shower#next + */ + next(isForce) { + const next = new Event('next', { cancelable: !isForce }); + if (this.dispatchEvent(next)) { + this.goBy(1); + } + } + + first() { + this.goTo(0); + } + + last() { + this.goTo(this.slides.length - 1); + } + } + + const options = document.currentScript.dataset; + const shower = new Shower(options); + + Object.defineProperty(window, 'shower', { + value: shower, + configurable: true, + }); + + contentLoaded(() => { + shower.start(); + }); + +})(); diff --git a/presentations/tpac-2025/Templates/simple-car-icon.svg b/presentations/tpac-2025/Templates/simple-car-icon.svg new file mode 100644 index 0000000..50e6372 --- /dev/null +++ b/presentations/tpac-2025/Templates/simple-car-icon.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + + + + + + + + + diff --git a/presentations/tpac-2025/Templates/slide-dark.svg b/presentations/tpac-2025/Templates/slide-dark.svg new file mode 100644 index 0000000..282acc1 --- /dev/null +++ b/presentations/tpac-2025/Templates/slide-dark.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentations/tpac-2025/Templates/slide.svg b/presentations/tpac-2025/Templates/slide.svg new file mode 100644 index 0000000..fab429d --- /dev/null +++ b/presentations/tpac-2025/Templates/slide.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentations/tpac-2025/Templates/slides.css b/presentations/tpac-2025/Templates/slides.css new file mode 100644 index 0000000..28fb706 --- /dev/null +++ b/presentations/tpac-2025/Templates/slides.css @@ -0,0 +1,1184 @@ +/* Style for the slides for TPAC 2025, to be used together with the + Shower script or the b6+ script. For usage instructions, see + https://www.w3.org/2025/Talks/TPAC/Templates/Overview.html + + TODO: Styles for blockquotes? + + TODO: Provide a fallback for side images for UAs that do not + implement 'object-fit'? + + TODO: .greeked is visually hidden, but assistive technology still + sees it and speaks it. Can that be fixed? ('speak: never' has no + effect.) + + Layout of a slide: + + +-------+---------------------------------------+ + | LOGO | 3.5em | + | | +-------------------------------+ | + | | | | | ^ + | | | | | | + | 2.7em |1em| |2em| 23em + | | | | | | + | | | | | v + | | +-------------------------------+ | + | nr | 1em | + +-------+---------------------------------------+ + + A = 16/9 = aspect ratio + N = 23 = height in em (i.e., 21 lines + 2 x 1 em padding) + L = 2.7 = logo width in em + H = 86/120.31532 = logo aspect ratio (width/height) + w = N*A = width of slide in em + r = 2 = right padding + l = L + 1 = left padding + t = 3.5 = top padding + b = 1 = bottom pdding + + Created: 9 December 2024 (based on + https://www.w3.org/Talks/Tools/Shower3-2/humaaans.css) + + Author: Bert Bos + + Copyright © 2025 World Wide Web Consortium. All Rights Reserved. + This work is distributed under the W3C® Software License[1] in the + hope that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. + + [1] http://www.w3.org/Consortium/Legal/copyright-software +*/ + +@font-face { + font-family: My Lato; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(Lato-Italic.woff2) format("woff2"), + url(Lato-Italic.woff) format("woff"); + src: local(Lato Italic), local(Lato-Italic), + url(Lato-Italic.woff2) format("woff2"), + url(Lato-Italic.woff) format("woff")} + +@font-face { + font-family: My Lato; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(Lato-Regular.woff2) format("woff2"), + url(Lato-Regular.woff) format("woff"); + src: local(Lato Regular), local(Lato-Regular), + url(Lato-Regular.woff2) format("woff2"), + url(Lato-Regular.woff) format("woff")} + +@font-face { + font-family: My Lato; + font-style: normal; + font-weight: bold; + font-display: swap; + src: url(Lato-Bold.woff2) format("woff2"), + url(Lato-Bold.woff) format("woff"); + src: local(Lato Bold), local(Lato-Bold), + url(Lato-Bold.woff2) format("woff2"), + url(Lato-Bold.woff) format("woff"); +} +@font-face { + font-family: My Lato; + font-style: italic; + font-weight: bold; + font-display: swap; + src: url(Lato-BoldItalic.woff2) format("woff2"), + url(Lato-BoldItalic.woff) format("woff"); + src: local(Lato Bold Italic), local(Lato-BoldItalic), + url(Lato-BoldItalic.woff2) format("woff2"), + url(Lato-BoldItalic.woff) format("woff"); +} +@font-face { + font-family: My Montserrat; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url(Montserrat-BlackItalic.woff2) format("woff2"), + url(Montserrat-BlackItalic.woff) format("woff"); + src: local(Montserrat Black Italic), local(Montserrat-BlackItalic), + url(Montserrat-BlackItalic.woff2) format("woff2"), + url(Montserrat-BlackItalic.woff) format("woff"); +} +@font-face { + font-family: My Montserrat; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(Montserrat-Black.woff2) format("woff2"), + url(Montserrat-Black.woff) format("woff"); + src: local(Montserrat Black), local(Montserrat-Black), + url(Montserrat-Black.woff2) format("woff2"), + url(Montserrat-Black.woff) format("woff"); +} + +@font-face { + font-family: My Montserrat; + font-style: italic; + font-weight: bold; + font-display: swap; + src: url(Montserrat-BoldItalic.woff2) format("woff2"), + url(Montserrat-BoldItalic.woff) format("woff"); + src: local(Montserrat BoldItalic), local(Montserrat-BoldItalic), + url(Montserrat-BoldItalic.woff2) format("woff2"), + url(Montserrat-BoldItalic.woff) format("woff"); +} + +@font-face { + font-family: My Montserrat; + font-style: normal; + font-weight: bold; + font-display: swap; + src: url(Montserrat-Bold.woff2) format("woff2"), + url(Montserrat-Bold.woff) format("woff"); + src: local(Montserrat Bold), local(Montserrat-Bold), + url(Montserrat-Bold.woff2) format("woff2"), + url(Montserrat-Bold.woff) format("woff"); +} + + +/* Colors and backgrounds for light mode */ +html, .lightmode { + --index-bg: url(linen.png) #595b60; + --index-fg: white; + --address-fg: hsl(356,67%,40%); + --h3-fg: hsl(356,67%,40%); + --slide-fg: black; + --slide-bg: top / 100.1% auto no-repeat url(slide.svg), + left / 2.7em /*= L */ 100.1% repeat-y + linear-gradient(hsl(244,37%,92%),hsl(244,37%,92%)), + hsl(0,0%,98%); + --slide-text-shadow: 0 -1px hsl(0,0%,98%), 1px 0 hsl(0,0%,98%), + 0 1px hsl(0,0%,98%), -1px 0 hsl(0,0%,98%), -0.7px -0.7px hsl(0,0%,98%), + 0.7px -0.7px hsl(0,0%,98%), 0.7px 0.7px hsl(0,0%,98%), + -0.7px 0.7px hsl(0,0%,98%); + --cover-bg: center / auto 100.1% url(cover.svg) no-repeat hsl(0,0%,98%); + --cover-event-fg: hsl(250,19%,50%); + --final-bg: top / 100.1% auto no-repeat url(slide.svg), + left / 2.7em /*= L */ 100.1% repeat-y + linear-gradient(hsl(244,37%,92%),hsl(244,37%,92%)), + hsl(0,0%,98%); + --slide-number-fg: #000; + --comment-bg: black; + --comment-fg: white; + --long-comment-bg: white; + --long-comment-fg: black; + --toc-bg: hsl(0,0%,98%); + --toc-fg: black; + --toc-list-marker-fg: hsl(211,100%,17%); + --b6-canvas-fg: hsla(0,100%,45%,0.8); + --em-bg: hsl(62,100%,50%); + --code-bg: #eee; + --table-striped-bg: #eee; + --link-fg: #00c; + --visited-fg: #609; + --can-invert-filter: none; + --h1-h2-fg: hsl(250,19%,50%); +} +/* Colors and backgrounds for dark mode */ +@media (prefers-color-scheme: dark) { + html { + --index-bg: linear-gradient(#0005,#0005), url(linen.png) #595b60; + --index-fg: #eee; + --address-fg: hsl(356,90%,60%); + --h3-fg: hsl(356,90%,60%); + --slide-fg: white; + --slide-bg: top / 100.1% auto no-repeat url(slide-dark.svg), + left / 2.7em /*= L */ 100.1% repeat-y + linear-gradient(hsl(244,23%,27%),hsl(244,23%,27%)), + black; + --slide-text-shadow: 0 -1px #000, 1px 0 #000, 0 1px #000, -1px 0 #000, + -0.7px -0.7px #000, 0.7px -0.7px #000, 0.7px 0.7px #000, + -0.7px 0.7px #000; + --cover-bg: center / auto 100.1% url(cover-dark.svg) no-repeat black; + --cover-event-fg: hsl(250,19%,70%); + --final-bg: top / 100.1% auto no-repeat url(slide-dark.svg), + left / 2.7em /*= L */ 100.1% repeat-y + linear-gradient(hsl(244,23%,27%),hsl(244,23%,27%)), + black; + --slide-number-fg: white /*#000*/; + --comment-bg: #222; + --comment-fg: white; + --long-comment-bg: #111; + --long-comment-fg: #eee; + --toc-bg: black; + --toc-fg: hsl(0,0%,98%); + --toc-list-marker-fg: hsl(250,100%,90%); + --b6-canvas-fg: hsla(130,100%,60%,0.8); + --em-bg: hsl(322,100%,40%); + --code-bg: #444; + --table-striped-bg: #333; + --link-fg: #AEF; + --visited-fg: #EAD; + --can-invert-filter: invert(1); + --h1-h2-fg: hsl(251,9%,75%); + } +} +.darkmode { + --index-bg: linear-gradient(#0005,#0005), url(linen.png) #595b60; + --index-fg: #eee; + --address-fg: hsl(356,90%,60%); + --h3-fg: hsl(356,90%,60%); + --slide-fg: white; + --slide-bg: top / 100.1% auto no-repeat url(slide-dark.svg), + left / 2.7em /*= L */ 100.1% repeat-y + linear-gradient(hsl(244,23%,27%),hsl(244,23%,27%)), + black; + --slide-text-shadow: 0 -1px #000, 1px 0 #000, 0 1px #000, -1px 0 #000, + -0.7px -0.7px #000, 0.7px -0.7px #000, 0.7px 0.7px #000, + -0.7px 0.7px #000; + --cover-bg: center / auto 100.1% url(cover-dark.svg) no-repeat black; + --cover-event-fg: hsl(250,19%,70%); + --final-bg: top / 100.1% auto no-repeat url(slide-dark.svg), + left / 2.7em /*= L */ 100.1% repeat-y + linear-gradient(hsl(244,23%,27%),hsl(244,23%,27%)), + black; + --slide-number-fg: white /*#000*/; + --comment-bg: #222; + --comment-fg: white; + --long-comment-bg: #111; + --long-comment-fg: #eee; + --toc-bg: black; + --toc-fg: hsl(0,0%,98%); + --toc-list-marker-fg: hsl(250,100%,90%); + --b6-canvas-fg: hsla(130,100%,60%,0.8); + --em-bg: hsl(322,100%,40%); + --code-bg: #444; + --table-striped-bg: #333; + --link-fg: #AEF; + --visited-fg: #EAD; + --can-invert-filter: invert(1); + --h1-h2-fg: hsl(251,9%,75%); +} + + +/* Common layout independent of slide mode */ +html {font: 400 1em/1.3 My Lato, Carlito, Calibri, Open Sans, Helvetica Neue, + Helvetica, Symbola, Noto Sans Symbols, Liberation Sans, Arial, Noto Emoji, + sans-serif; + font-optical-sizing: none; /* make preview window and slide look the same */ + font-variant-ligatures: no-common-ligatures; + color-scheme: only light dark; /* only = disable Chromium's heuristics. */ + background: none; /* Make sure the background of body gets used */ + scroll-behavior: smooth; + font-size-adjust: 0.506 /* Lato Regular */; letter-spacing: 0.02em} +body {background: var(--index-bg); counter-reset: slide; + margin: 0.5em 2em 9em; color: var(--index-fg)} +b {font-weight: bold} +dt {font-weight: bold} +dd {margin: 0} +h1 {font-size: 2em; margin: 0.67em 0} +h4 {font-size: 1.2em; margin: 0.5em 0} +.slide p, .slide ul, .slide ol, .slide pre, .slide blockquote, .slide li { + margin: 0 0 0.6em 0} +.slide h1, .slide h2, .slide address {margin: 0 0 0.5em 0; + font: 700 2em/1.1 My Montserrat, Arial Black, Myriad Pro, Roboto, sans-serif; + font-size-adjust: 0.542; color: var(--h1-h2-fg)} +.slide address {color: var(--address-fg); font-size: 1.4em} +.slide address :link, .slide address :visited {color: inherit} +.slide h3 {font-size: 1.1em; color: var(--h3-fg); + margin: 0.8em 0 0.48em 0} +.full, .comment {width: 40.889em; /*= w */ height: 23em; /*= N */} +.slide {width: 40.889em; /*= w */ min-height: 23em; /*= N */ + color: var(--slide-fg); box-shadow: 0 2px 3px #000; + line-height: 1.6; + word-break: normal; overflow-wrap: normal; letter-spacing: normal; + padding: 3.5em /*= t */ 2em /*= r */ 1em /*= b */ 3.7em /*= l */; + position: relative; scroll-behavior: smooth; overflow: auto; + box-sizing: border-box; z-index: 0; display: inline-block; + margin: 4em 2em 0 0; vertical-align: top; counter-increment: slide; + text-shadow: var(--slide-text-shadow); + background: var(--slide-bg)} +.slide.textfit, .textfit .slide {height: 23em /*= N */} +.clear .slide:not(.cover), .slide:not(.cover).clear {background-image: none} +.slide:target {outline: lime solid 0.5em; outline-offset: 1em} +.slide h3 a {color: inherit} +.slide :link {color: var(--link-fg)} +.slide :visited {color: var(--visited-fg)} + +/* EM elements get a highlighter-like background */ +.slide em {font-style: normal; padding-left: 0.1em; padding-right: 0.1em; + text-shadow: none; background: var(--em-bg)} + +/* Lists have less indent than the default. */ +.slide li {margin-left: 1em} +.slide ol, .slide ul {padding: 0} +.slide li ul, .slide li ol, .slide li li {margin-top: 0.1em; + margin-bottom: 0.2em} + +/* Own counter, because FF & Safari don't apply text-shadow to the default. */ +.slide ol {counter-reset: ol; list-style: none} +.slide ol > li {counter-increment: ol} +.slide ol > li::before {/*float: left;*/ display: inline-block; width: 2em; + margin-left: -2em; text-align: right; content: counter(ol) ".\A0"} +.slide ol > li > p:first-child {display: inline} +.slide ol[start="2"] {counter-reset: ol 1} +.slide ol[start="3"] {counter-reset: ol 2} +.slide ol[start="4"] {counter-reset: ol 3} +.slide ol[start="5"] {counter-reset: ol 4} +.slide ol[start="6"] {counter-reset: ol 5} +.slide ol[start="7"] {counter-reset: ol 6} +.slide ol[start] {counter-reset: ol calc(attr(start integer) - 1)} + +/* Lists with icons or small images instead of bullets. The first + child of each LI becomes a list bullet. */ +ul.with-icons > li {margin-left: 1.5em; list-style: none} +ul.with-icons > li > *:first-child {display: inline-block; + white-space-trim: discard-before discard-after; /* experimental property */ + margin: 0 0.5em 0 -1.5em; width: 1em} + +/* Slides with an image on the left (.side) or right (.side.right) one + third. Use percentages for the size and position of the image, + because the em may be too small if the font size is reduced due to + class=textfit */ +.slide.side {padding-left: 15.257em /*= l + (w - l - r) * 30% + 1 */} +.slide.side.right, .slide.side.r {padding-left: 3.7em /*= l */; + padding-right: 13.557em /*= r + (w - l - r) * 30% + 1 */} +.side .side {position: absolute; top: 15.217% /*= t / N */; + left: 9.0489% /*= l / w */; + height: 80.435% /*= (N - t - b) / N */; object-fit: contain; + width: 25.818% /*= (w - l - r) * 30% / w */} +.side .side.cover {object-fit: cover; top: 0; left: 0; height: 100%; + width: 34.867% /*= ((w - l - r) * 30% + l) / w */} +.side.right .side, .side.r .side { + left: 69.291% /*= (w - r - (w - l - r) * 30%) / w */} +.side.right .side.cover, .side.r .side.cover { + width: 30.709% /*= (r + (w - l - r) * 30%) / w */} + +/* Slides with a big, square image on the left or right */ +.slide.side.big {padding-left: 23.2em /*= l + (N - t - b) + 1 */} +.slide.side.right.big, .slide.side.r.big {padding-left: 3.7em /*= l */; + padding-right: 21.5em /*= r + (N - t - b) + 1 */} +.side.big .side {top: 15.217% /*= t/N */; left: 9.0489% /*= l/w */; + height: 80.435% /*= (N - t - b)/N */; width: 45.245% /*= (N - t - b)/w */} +.side.big.right .side, .side.big.r .side {left: 49.864% /*= (w - r - (N - t - b))/w */} +.side.big .side.cover {object-fit: cover; top: 0; left: 0; + height: 100%; width: 54.293% /*= (N - t - b + l)/w */} +.side.big.right .side.cover, .side.big.r .side.cover { + left: 49.864% /*= (w - (N - t - b) - r)/w */; + width: 50.136% /*= ((N - t - b) + r)/w */} + +/* Cover pages */ +.slide.cover {padding: 10.5em 3.7em /*= l */ 1em /*= b */ 3.7em /*= l */; + background: var(--cover-bg)} +.slide.cover h1 {} +.slide.cover address {} +.slide.cover p.event {line-height: 1.3; color: var(--cover-event-fg); + text-align: center; font-size: 0.8em; font-weight: bold; text-shadow: none; + position: absolute; top: 5.9em; right: 1em} +.slide.cover p::first-line {color: transparent} + +/* Last page */ +.slide.final {background: var(--final-bg)} + +/* Notes in a smaller font */ +.slide .note {font-size: 70%} + +/* Miscellaneous styles */ +.num {font-variant-numeric: oldstyle-nums tabular-nums diagonal-fractions} +.slide code, .slide pre {font-family: Andale Mono, Courier, monospace; + text-shadow: none} +.slide code {background: var(--code-bg); padding: 0.1em 0.3em; + border-radius: 0.3em} +.slide pre code {background: none; padding: 0; font: inherit} /* Reset */ +sub, sup {line-height: 0.5} +.slide pre {padding: 0 0.2em; background: black; color: hsl(120,100%,70%); + text-shadow: none; line-height: 1.5} +.icon {height: 1em; max-width: 1em; vertical-align: text-bottom} + +/* Explicit placement on a 3x3 grid */ +.place {position: absolute; box-sizing: border-box; + max-width: 27.056%; /*= (w - l - r - 2) / 3 / w */ + top: 50%; left: 52.079%; /*= (l + (w - l - r) / 2) / w */ + transform: translate(-50%, -50%); text-align: center} +.place.t, .place.top {top: 15.217%; /*= t/N */ transform: translate(-50%,0)} +.place.b, .place.bottom {top: auto; bottom: 4.3478% /*= b/N */; + transform: translate(-50%,0)} +.place.l, .place.left {left: 9.0489%; /*= l / w */ + transform: translate(0,-50%); text-align: left} +.place.r, .place.right {left: auto; right: 4.8913%; /*= r / w */ + transform: translate(0,-50%); text-align: right} +.place.t.l, .place.top.left, .place.t.r, .place.top.right, .place.b.l, +.place.bottom.left, .place.b.r, .place.bottom.right {transform: none} + +/* Numbered lines in a PRE */ +pre.numbered {padding-left: 2em; overflow-y: hidden; position: relative} +pre.numbered::before {color: #aaa; text-align: right; white-space: pre-line; + text-shadow: none; + content: "1\A 2\A 3\A 4\A 5\A 6\A 7\A 8\A 9\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A 18\A 19\A 20"; + position: absolute; top: 0; left: 0; width: 1.2em; font-family: serif; + border-right: thin solid; padding-right: 0.2em} + +/* Full-size image overlays */ +img.cover, img.fit, video.cover, video.fit {position: absolute; z-index: -1; + top: 0; left: 0; + width: 100%; height: 100%; object-fit: cover; padding: 0} +img.fit, video.fit {object-fit: contain} + +/* Slide number in lower left corner */ +.slide::before {content: counter(slide); position: absolute; + left: 0; width: 2.7em /*= L */; bottom: 1em /*= b */; text-shadow: none; + font-weight: bold; + text-align: center; color: var(--slide-number-fg)} +.slide.cover::before {} +.slide.final::before {} +.full .slide::before {position: fixed} /* In case the slide scrolls */ +.clear .slide::before, .slide.clear::before {content: none} + +/* Two columns, and alternate elements in the left and right column */ +.slide .columns > * {box-sizing: border-box; margin-top: 0; margin-bottom: 0; + width: 47.158% /*= (w - l - r - 2) / 2 / (w - l - r) */; float: right} +.slide .columns > *:nth-child(odd) {clear: both; float: left} +.slide .columns {overflow: hidden; line-height: 1.5 /* Reduced from 1.6 */} +.slide .columns > * > *:first-child {margin-top: 0} +.slide .columns > * > *:last-child {margin-bottom: 0} +@supports (display: grid) { + .slide .columns {overflow: visible; display: grid; grid: "a b" / 1fr 1fr; + grid-gap: 0.6em 2em; justify-items: normal} + .slide .columns > * {width: auto} +} +@supports not (display: grid) { + /* If grid is not supported and the column is a list, remove the margin */ + .slide .columns > li {margin-left: 0; list-style-position: inside} + .slide .columns > *:nth-child(n+3) {margin-top: 0.6em} /* gap between rows */ +} + +/* A trick that may be useful for people who insist on putting a lot + of text on a slide: class "compact" can + be set on a list or other container and removes the top and bottom + margin from list items and paragraphs inside that container. */ +.slide .compact li, .slide .compact p {margin-top: 0; margin-bottom: 0} + +/* Striped tables */ +table {margin: 0 0 0.6em} +table.striped {border-collapse: collapse; margin-bottom: 0.48em; width: 100%} +table.striped td, table.striped th {padding: 0.15em 0.3em; font-size: 0.93em; + text-align: left} +table.striped tr:nth-child(2n+2) {background: var(--table-striped-bg); + text-shadow: none} + +/* Tables with borders () */ +table[border] {border-collapse: collapse} +table[border], table[border] thead, table[border] tbody, table[border] tfoot, +table[border] tr, table[border] td, table[border] th {border-width: thin; +border-style: solid} +table[border] td, table[border] th {padding: 0.1em 0.3em} + +/* Takahashi method (very big text, very few words) */ +.shout {font-size: 400%; line-height: 1.1; text-wrap: balance} +p.shout {margin: 0.25em 0} + +/* Figures, and images with collapsed descriptions */ +img {max-width: 100%;} +figure {text-align: center; margin: 0 0 0.6em 0} +figure img:not(.cover):not(.fit), summary img {display: block; + margin: 0 auto 0.6em auto; + max-height: 14.7em /*= N - b - t - 1.1 * 2 - 0.5 * 2 - 0.6 */} +.slide summary {list-style: none} /* Hide the triangle */ +.slide summary::-webkit-details-marker {display: none} /* Ditto webkit/blink */ +.slide [open] summary img {max-height: 4em} +.slide summary {outline: none} +.slide summary::before {content: "⊖"; float: left; width: 0.9em; + margin-left: -1.1em; text-align: left; line-height: 0.9} +.slide [open] > summary::before {content: "⊕"} +.slide summary:focus::before {outline: thin solid blue; + outline: thin solid invert} +.can-invert {filter: var(--can-invert-filter)} + +/* Keyboard keys */ +kbd {font-weight: bold; speak-as: spell-out} + +/* The progress element is normally empty */ +.progress {display: inline} + +/* Notes between the slides */ +.comment {background: var(--comment-bg); color: var(--comment-fg); padding: 1em; + font-family: Times New Roman, Times, serif; box-sizing: border-box; + display: inline-block; border-radius: 0.5em; margin: 4em 2em 0 0; + box-shadow: 0 2px 3px #000; vertical-align: top; overflow: auto} +.comment :link, .comment :visited {color: inherit; text-decoration: underline} +.comment pre {margin-left: 1em; font-family: Helvetica, sans-serif} +.comment :first-child {margin-top: 0} +.comment dd, .comment ul, .comment ol {padding-left: 1em; margin-left: 0} +.comment dd {margin-bottom: 1em} +.comment h1, .comment h2, .comment h3, .comment h4, .comment h5, .comment h6 { + break-after: avoid} +.slide ~ .comment::before {content: "notes for slide " counter(slide); + display: block; + text-align: center; font-size: small; font-variant: small-caps; + border-bottom: thin solid; padding-bottom: 0.3em; margin-bottom: 1em} + +/* Long comments */ +.comment.long {height: auto; display: block; border-radius: 0; overflow: auto; + background: var(--long-comment-bg); color: var(--long-comment-fg)} +.comment.long:before {content: none} + +/* A list as a tree structure with box-drawing characters */ +.tree {white-space: nowrap; line-height: 1.5; padding: 0; + overflow-x: auto; overflow-y: hidden} +.tree ul {padding: 0} +.tree li {display: block} + +.tree li::before {content: "├ "; font-size: 1.2em; line-height: 0.5; + vertical-align: middle} +.tree li:last-child::before {content: "└ "} + +.tree li li::before {content: "│\2002├ "} +.tree li li:last-child::before {content: "│\2002└ "} +.tree li:last-child li::before {content: "\2002\2002├ "} +.tree li:last-child li:last-child::before {content: "\2002\2002└ "} + +.tree li li li::before {content: "│\2002│\2002├ "} +.tree li li li:last-child::before {content: "│\2002│\2002└ "} +.tree li li:last-child li::before {content: "│\2002\2002\2002├ "} +.tree li li:last-child li:last-child::before {content: "│\2002\2002\2002└ "} +.tree li:last-child li li::before {content: "\2002\2002│\2002├ "} +.tree li:last-child li li:last-child::before {content: "\2002\2002│\2002└ "} +.tree li:last-child li:last-child li::before {content: "\2002\2002\2002\2002├ "} +.tree li:last-child li:last-child li:last-child::before {content: "\2002\2002\2002\2002└ "} + +.tree li li li li::before {content: "│\2002│\2002│\2002├ "} +.tree li li li li:last-child::before {content: "│\2002│\2002│\2002└ "} +.tree li li li:last-child li::before {content: "│\2002│\2002\2002\2002├ "} +.tree li li li:last-child li:last-child::before {content: "│\2002│\2002\2002\2002└ "} +.tree li li:last-child li li::before {content: "│\2002\2002\2002│\2002├ "} +.tree li li:last-child li li:last-child::before {content: "│\2002\2002\2002│\2002└ "} +.tree li li:last-child li:last-child li::before {content: "│\2002\2002\2002\2002\2002├ "} +.tree li li:last-child li:last-child li:last-child::before {content: "│\2002\2002\2002\2002\2002└ "} +.tree li:last-child li li li::before {content: "\2002\2002│\2002│\2002├ "} +.tree li:last-child li li li:last-child::before {content: "\2002\2002│\2002│\2002└ "} +.tree li:last-child li li:last-child li::before {content: "\2002\2002│\2002\2002\2002├ "} +.tree li:last-child li li:last-child li:last-child::before {content: "\2002\2002│\2002\2002\2002└ "} +.tree li:last-child li:last-child li li::before {content: "\2002\2002\2002\2002│\2002├ "} +.tree li:last-child li:last-child li li:last-child::before {content: "\2002\2002\2002\2002│\2002└ "} +.tree li:last-child li:last-child li:last-child li::before {content: "\2002\2002\2002\2002\2002\2002├ "} +.tree li:last-child li:last-child li:last-child li:last-child::before {content: "\2002\2002\2002\2002\2002\2002└ "} + +/* Layout in slide mode (when body has class=full) */ +.full {transform: scale(var(--shower-full-scale))} /* For Shower 3.1/3.2 */ +.full, .full .slide {position: absolute; overflow: hidden} +.full .slide {height: 23em; /*= N */} +.full {top: 50%; left: 50%; background: black; + margin: -11.5em /*= -N/2 */ 0 0 -20.444em /*= -w/2 */} +.full .slide {visibility: hidden; top: 0; left: 0; margin: 0} +.full .slide.active {visibility: visible} +.full .comment {display: none} +.full .slide:target {outline: none} + +/* Progress bar. A data-timing attribute on body indicates the slide + show is automatic and the class "manual" says it is currently + paused. */ +.full .progress {position: absolute; bottom: 0; left: 0; height: 1px; + background: linear-gradient(to right, hsla(0,100%,50%,0),hsla(0,100%,50%,1)); + z-index: 1} +[data-timing].manual::before {position: absolute; z-index: 1; + content: url("data:image/svg+xml,"); + top: calc(50% - 2em); left: calc(50% - 2em); width: 4em; height: 4em} +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + .full .progress {transition: 0.5s} +} + +/* Incremental display with elements replacing each other. In index + mode, the elements are side by side with a scroll bar to reach them + (and scroll snap to make scrolling easier). In slide mode, all + items are in the first slot, but at most one of them is visible. */ +.incremental.in-place, .overlay.in-place {display: grid; grid: "a" / 100%; + gap: 2em; grid-auto-columns: 100%; grid-auto-flow: column; + overflow: auto; scrollbar-width: thin; scroll-snap-type: x mandatory} +.incremental.in-place > *, .overlay.in-place > * {scroll-snap-align: end} +.full .incremental.in-place > *, .full .overlay.in-place > * {grid-area: a} +.full .incremental.in-place > .visited:not(.active):not(:last-child), +.full .overlay.in-place > .visited:not(.active):not(:last-child) { + visibility: hidden} + +/* Reveal elements one by one. (incremental/overlay only works with b6+) */ +.full .incremental > :not(.active):not(.visited), +.full .overlay > :not(.active):not(.visited), +.full .next:not(.active):not(.visited) {visibility: hidden} + +/* With class=greeked, elements aren't hidden, but shown as gray bars */ +.full .incremental > .greeked:not(.active):not(.visited), +.full .incremental.greeked > :not(.active):not(.visited), +.full .greeked .incremental > :not(.active):not(.visited), +.full.greeked .incremental > :not(.active):not(.visited), +.full .overlay > .greeked:not(.active):not(.visited), +.full .overlay.greeked > :not(.active):not(.visited), +.full .greeked .overlay > :not(.active):not(.visited), +.full.greeked .overlay > :not(.active):not(.visited), +.full .next.greeked:not(.active):not(.visited), +.full .greeked .next:not(.active):not(.visited), +.full.greeked .next:not(.active):not(.visited) {visibility: inherit; + text-shadow: none; background: hsl(0,0%,50%); color: transparent; + speak: never} + +/* With class=strong, the currently active element is red. */ +.full .incremental .active.strong, .full .overlay .active.strong, +.full .incremental.strong .active, .full .overlay.strong .active, +.full .strong .incremental .active, .full .strong .overlay .active, +.full.strong .incremental .active, .full.strong .overlay .active, +.full .strong .next.active, .full .next.active.strong, +.full.strong .next.active {color: hsl(356,75%,53%)} + +/* With class=dim, elements that are no longer active are grayed out. */ +.full .incremental > .visited.dim, +.full .incremental.dim > .visited, +.full .dim .incremental > .visited, +.full.dim .incremental > .visited, +.full .overlay > .visited.dim, +.full .overlay.dim > .visited, +.full .dim .overlay > .visited, +.full.dim .overlay > .visited, +.full .next.visited.dim, +.full .dim .next.visited, +.full.dim .next.visited {opacity: 0.3} + +/* Classes if-b6plus and if-not-b6plus are for elements that should + only be shown if b6+ is in use, resp. not in use. (The latter + probably means that Shower is used instead.) */ +body:not(.b6plus) .if-b6plus {display: none} +body.b6plus .if-not-b6plus {display: none} + +/* Animate the active element when it appears. By default, the element + is progressively revealed, starting from the left. Setting + class=emerge instead causes the element to go from transparent to + opaque. And class=quick omits the animation. The class can be set + on the element itself or on any ancestor, including on BODY. .*/ +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + .full .incremental > .active, .full .overlay > .active, + .full .next.active {animation: unfold 1s} + .full .incremental > .active.emerge, .full .overlay > .active.emerge, + .full .incremental.emerge > .active, .full .overlay.emerge > .active, + .full .emerge .incremental > .active, .full .emerge .overlay > .active, + .full.emerge .incremental > .active, .full.emerge .overlay > .active, + .full .emerge .next.active, .full .next.active.emerge, + .full.emerge .next.active {animation: fade-in 0.5s} + .full .incremental .active.quick, .full .overlay .active.quick, + .full .incremental.quick .active, .full .overlay.quick .active, + .full.quick .incremental .active, .full.quick .overlay .active, + .full .quick .incremental .active, .full .quick .overlay .active, + .full .quick .next.active, .full .next.active.quick, + .full.quick .next.active {animation: none} +} + +@keyframes unfold { + from {clip-path: inset(0% 100% 0% -100%)} + to {clip-path: inset(0% 0% 0% -100%)} +} + +/* Animation of a slowly growing element */ +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + .full .grow {transition: 3s 1s ease-in-out transform; + position: relative; transform: scale(0.1); transform-origin: 0 50%} + .active .grow {transform: scale(1)} +} + +/* Transitions between slides */ +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + + .full .slide.active ~ .visited {animation: none} /* Moving backwards */ + + /* Transition: fade-in */ + .full .slide.fade-in.visited, + .fade-in .slide.visited {animation: delay 1s 1} + .full .slide.fade-in + .active, + .full .slide.fade-in + .comment + .active, + .fade-in .slide.active {animation: fade-in 1s 1} + @keyframes delay { + from {visibility: visible} + to {visibility: visible} + } + @keyframes fade-in { + from {opacity: 0} + to {opacity: 1} + } + + /* Transition: slide-in */ + .full .slide.slide-in.visited, + .slide-in .slide.visited {animation: leftout 1s 1} + .full .slide.slide-in + .active, + .full .slide.slide-in + .comment + .active, + .slide-in .slide.active {animation: leftin 1s 1} + @keyframes leftout { + from {transform: translate(0%, 0); visibility: visible; z-index: 1} + to {transform: translate(-100%, 0); visibility: visible; z-index: 1} + } + @keyframes leftin { + from {transform: translate(-100%, 0); visibility: visible} + to {transform: translate(0%, 0); visibility: visible} + } + + /* Transition: slide-out */ + .full .slide.slide-out.visited, + .slide-out .slide.visited {animation: leftout 1s 1} + .full .slide.slide-out + .active, + .full .slide.slide-out + .comment + .active, + .slide-out .slide.active {animation: do-nothing 1s 1} + @keyframes do-nothing { + from {z-index: 0} + to {z-index: 0} + } + + /* Transition: move-left */ + .full .slide.move-left.visited, + .move-left .slide.visited {animation: leftout 1s 1} + .full .slide.move-left + .active, + .full .slide.move-left + .comment + .active, + .move-left .slide.active {animation: rightin 1s 1} + @keyframes rightin { + from {transform: translate(100%, 0); visibility: visible} + to {transform: translate(0%, 0); visibility: visible} + } + + /* Transition: slide-up */ + .full .slide.slide-up.visited, + .slide-up .slide.visited {animation: topout ease-in 1s 1} + .full .slide.slide-up + .active, + .full .slide.slide-up + .comment + .active, + .slide-up .slide.active {animation: do-nothing ease-in 1s 1} + @keyframes topout { + from {transform: translate(0, 0%); visibility: visible; z-index: 1} + 80% {opacity: 1.0} + to {transform: translate(0, -100%); visibility: visible; opacity: 0.0; + z-index: 1} + } + + /* Transition: move-up */ + .full .slide.move-up.visited, + .move-up .slide.visited {animation: topout ease-in 1s 1} + .full .slide.move-up + .active, + .full .slide.move-up + .comment + .active, + .move-up .slide.active {animation: bottomin ease-in 1s 1} + @keyframes bottomin { + from {transform: translate(0, 100%); visibility: visible} + to {transform: translate(0, 0%); visibility: visible} + } + + /* Transition: flip-up */ + .full {perspective: 1000px; perspective: 1000} + .full .slide.flip-up.visited, + .flip-up .slide.visited {animation: turn-down 1s 1 ease-in} + .full .slide.flip-up + .active, + .full .slide.flip-up + .comment + .active, + .flip-up .slide.active {animation: turn-up 1s 1 ease-out} + @keyframes turn-down { + from {transform: rotateX(0deg); visibility: visible} + 50%, to {transform: rotateX(90deg); visibility: hidden} + } + @keyframes turn-up { + from, 50% {transform: rotateX(-90deg); visibility: visible} + to {transform: rotateX(0deg); visibility: visible} + } + + /* Transition: flip-left */ + .full .slide.flip-left.visited, + .flip-left .slide.visited {animation: flip-left1 1s 1 ease-in} + .full .slide.flip-left + .active, + .full .slide.flip-left + .comment + .active, + .flip-left .slide.active {animation: flip-left2 1s 1 ease-out} + @keyframes flip-left1 { + from {transform: rotateY(0deg); visibility: visible} + 50%, to {transform: rotateY(-90deg); visibility: hidden} + } + @keyframes flip-left2 { + from, 50% {transform: rotateY(90deg); visibility: visible} + to {transform: rotateY(0deg); visibility: visible} + } + + /* Transition: center-out */ + .full .slide.center-out.visited, + .center-out .slide.visited {animation: gray 1s 1} + .full .slide.center-out + .active, + .full .slide.center-out + .comment + .active, + .center-out .slide.active {animation: center-out 1s 1} + @keyframes gray { + from, to {opacity: 0.5; visibility: visible} + } + @keyframes center-out { + from {clip-path: circle(0)} + to {clip-path: circle(100%)} + } + + /* Transition: wipe-left */ + .full .slide.wipe-left.visited, + .wipe-left .slide.visited {animation: gray 1s 1} + .full .slide.wipe-left + .active, + .full .slide.wipe-left + .comment + .active, + .wipe-left .slide.active {animation: rightin 1s 1} + + /* Transition: zigzag-left */ + .full .slide.zigzag-left.visited, + .zigzag-left .slide.visited {animation: gray 1s 1} + .full .slide.zigzag-left + .active, + .full .slide.zigzag-left + .comment + .active, + .zigzag-left .slide.active {animation: zigzag-left 1s 1} + @keyframes zigzag-left { + from {clip-path: + polygon(120% 0%, 120% 0%, 100% 30%, 120% 60%, 110% 100%, 120% 100%)} + to {clip-path: + polygon(120% 0%, 0% 0%, -20% 30%, 0% 60%, -10% 100%, 120% 100%)} + } + + /* Transition: zigzag-right */ + .full .slide.zigzag-right.visited, + .zigzag-right .slide.visited {animation: gray 1s 1} + .full .slide.zigzag-right + .active, + .full .slide.zigzag-right + .comment + .active, + .zigzag-right .slide.active {animation: zigzag-right 1s 1} + @keyframes zigzag-right { + from {clip-path: + polygon(-20% 0%, -20% 0%, 0% 30%, -20% 60%, -10% 100%, -20% 100%)} + to {clip-path: + polygon(-20% 0%, 100% 0%, 120% 30%, 100% 60%, 110% 100%, -20% 100%)} + } + + /* Transition: cut-in */ + .full .slide.cut-in.visited, + .cut-in .slide.visited {animation: gray 1s 1} + .full .slide.cut-in + .active, + .full .slide.cut-in + .comment + .active, + .cut-in .slide.active {animation: cut-in 1s 1} + @keyframes cut-in { + from {transform: translate(-100%, -100%)} + to {transform: translate(0%, 0%)} + } + + /* Transition: assemble */ + .full .slide.assemble + .active > *:nth-child(8n+1), + .full .slide.assemble + .comment + .active > *:nth-child(8n+1), + .assemble .slide.active > *:nth-child(8n+1) {animation: assemble1 1.2s 1} + .full .slide.assemble + .active > *:nth-child(8n+2), + .full .slide.assemble + .comment + .active > *:nth-child(8n+2), + .assemble .slide.active > *:nth-child(8n+2) {animation: assemble2 0.8s 1} + .full .slide.assemble + .active > *:nth-child(8n+3), + .full .slide.assemble + .comment + .active > *:nth-child(8n+3), + .assemble .slide.active > *:nth-child(8n+3) {animation: assemble3 0.7s 1} + .full .slide.assemble + .active > *:nth-child(8n+4), + .full .slide.assemble + .comment + .active > *:nth-child(8n+4), + .assemble .slide.active > *:nth-child(8n+4) {animation: assemble4 0.85s 1} + .full .slide.assemble + .active > *:nth-child(8n+5), + .full .slide.assemble + .comment + .active > *:nth-child(8n+5), + .assemble .slide.active > *:nth-child(8n+5) {animation: assemble5 1.1s 1} + .full .slide.assemble + .active > *:nth-child(8n+6), + .full .slide.assemble + .comment + .active > *:nth-child(8n+6), + .assemble .slide.active > *:nth-child(8n+6) {animation: assemble6 0.9s 1} + .full .slide.assemble + .active > *:nth-child(8n+7), + .full .slide.assemble + .comment + .active > *:nth-child(8n+7), + .assemble .slide.active > *:nth-child(8n+7) {animation: assemble7 1s 1} + .full .slide.assemble + .active > *:nth-child(8n+8), + .full .slide.assemble + .comment + .active > *:nth-child(8n+8), + .assemble .slide.active > *:nth-child(8n+8) {animation: assemble8 0.95s 1} + @keyframes assemble5 { + from {transform: translate(2rem, -23rem /*= -N */) rotate(-18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + @keyframes assemble4 { + from {transform: translate(40.889rem /*= w */, -23rem /*= -N */) rotate(18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + @keyframes assemble3 { + from {transform: translate(40.889rem /*= w */, 2rem) rotate(-18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + @keyframes assemble1 { + from {transform: translate(40.889rem /*= w */, 23rem /*= N */) rotate(18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + @keyframes assemble8 { + from {transform: translate(-2rem, 23rem /*= N */) rotate(-18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + @keyframes assemble2 { + from {transform: translate(-40.889rem /*= -w */, 23rem /*= N */) rotate(18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + @keyframes assemble7 { + from {transform: translate(-40.889rem /*= -w */, -2rem) rotate(-18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + @keyframes assemble6 { + from {transform: translate(-40.889rem /*= -w */, -23rem /*= -N */) rotate(18deg)} + to {transform: translate(0, 0) rotate(0deg)} + } + +} /* End of @media not screen and (prefers-reduced-motion: reduce) */ + +/* A section with aria-live=assertive, which should be spoken, but not + displayed. (b6+ adds this style by itself, but Shower relies on the + style sheet setting it.)*/ +[role=region][aria-live=assertive] {position: absolute; top: 0; left: 0; + clip: rect(0 0 0 0); clip-path: rect(0 0 0 0)} + +/* Trick: If the viewport is exactly w x h or 1.2w x 1.2h, it is + almost certain that the slides are being shown inside an iframe of + that size. In that case, and if there is a targeted slide + ('.slide:target' exists), but b6+ is not running + ('body:not(.b6plus)'), hide everything except the targeted slide. + Also omit the black background, which would otherwise be visible + around the rounded corner of the slide. (When JavaScript is on, + adding ?full to the end of the slide URL, e.g., + ".../myslides.html?full&static#intro", has a similar effect and + doesn't require the iframe to be this exact size.) */ +@media (min-width: 40.839em /*= w - 0.05 */) and + (max-width: 40.939em /*= w + 0.05 */) and + (min-height: 22.95em /*= N - 0.05 */) and + (max-height: 23.05em /*= N + 0.05 */), + (min-width: 49.017em /*= 1.2 * w - 0.05 */) and + (max-width: 49.117em /*= 1.2 * w + 0.05 */) and + (min-height: 27.55em /*= 1.2 * N - 0.05 */) and + (max-height: 27.65em /*= 1.2 * N + 0.05 */) { + html:has(.slide:target) {font-size: calc(100vh / 23)} + body:not(.b6plus):has(.slide:target) {margin: 0; overflow: hidden; + background: transparent} + body:not(.b6plus):has(.slide:target) > *:not(.slide), + body:not(.b6plus):has(.slide:target) > .slide:not(:target) { + visibility: hidden; position: absolute} + body:not(.b6plus):has(.slide:target) > .slide { + box-shadow: none; margin: 0; outline: none} +} + +/* class=framed is used to indicate the slides are inside an iframe. */ +body.framed {background: transparent} +body.framed .slide {box-shadow: none} +body.framed .progress {display: none} + +/* When BODY has class has-2nd-window, it means the window is a + preview window (b6+ only). Show only the clock, the navigation + buttons, the current slide and the next slide, and the comments for + the current slide. The current slide occupies 3/5th of the width, + the next slide 2/5th. */ +.has-2nd-window { + margin: 0.5rem; height: calc(100vh - 1rem); display: grid; + grid: "current next next" auto + "notes notes notes" 1fr + "buttons buttons clock" auto + / 1fr auto auto; + gap: 0.5rem} +.has-2nd-window .clock, .has-2nd-window .fullclock { + position: relative; grid-area: clock; align-self: start; margin: 0; + right: 0; top: 0; border: thin solid; box-shadow: none} +.has-2nd-window .b6-ui { + grid-area: buttons; margin: 0; top: 0; + border: thin solid; position: static; border-radius: 0.5rem; + background: none; color: #ddd; padding: 0; box-shadow: none} +.has-2nd-window .slide, .has-2nd-window .comment { + position: absolute; left: -100%; clip-path: rect(0 0 0 0); margin: 0} +.has-2nd-window .slide { + font-size: calc((100vw - 2rem - 17px) / 68.148 /*= 5/3 * w */)} +.has-2nd-window .slide.slide.slide.slide {animation: none} +.has-2nd-window .comment { + height: 0; background: none; color: inherit; box-shadow: none; + font-size: x-large} +.has-2nd-window .slide ~ .comment::before { + content: "[" counter(slide) " of " counter(numslides) "]"; + text-align: left; border-bottom: none; padding-bottom: 0} +.has-2nd-window .slide.active { + position: relative; left: 0; clip-path: none; grid-area: current} +.has-2nd-window .slide.active + .comment { + position: relative; left: 0; clip-path: none; height: auto; + grid-area: notes; width: auto} +.has-2nd-window .slide.active + .slide, +.has-2nd-window .slide.active + .comment + .slide { + position: relative; left: 0; clip-path: none; + grid-area: next; align-self: center; + font-size: calc((100vw - 2rem - 17px) / 102.22 /*= 5/2 * w */)} +.has-2nd-window .slide:target {outline: none} + +/* Outline elements on the second window that are incrementally + displayed on the first window (b6+) */ +.has-2nd-window .slide.active .incremental > *, +.has-2nd-window .slide.active .overlay > *, +.has-2nd-window .slide.active .next {outline: thin dashed red} + +/* Style for clocks on the second window or in index mode. */ +body {--time-factor: 0} /* Make sure it is defined, will be set by b6+ */ +.fullclock, .clock {position: fixed; z-index: 1; top: 0.5em; right: 0.5em; + background: linear-gradient(hsl(120,90%,20%), + hsl(120,80%,25%), hsl(120,90%,19%)); color: #fff; border-radius: 0.5em; + box-shadow: 0 2px 3px #000; text-align: center; width: fit-content} +.fullclock:empty, .clock:empty {display: none} /* Shower doesn't make clocks */ +.fullclock {padding: 0.3em; display: grid; justify-items: center; gap: 0.1em; + grid: "x y z" auto + "a b d" auto + "f c e" auto + "h c g" auto + / 1fr 1fr 1fr} +.fullclock time:nth-of-type(1) {grid-area: a; color: #9F9} +.fullclock time:nth-of-type(2) {grid-area: b} +.fullclock time:nth-of-type(3) {grid-area: d} +.fullclock .timepause {grid-area: g} +.fullclock .timeinc {grid-area: e} +.fullclock .timedec {grid-area: f} +.fullclock .timereset {grid-area: h} +.fullclock i:nth-of-type(1) {grid-area: x} +.fullclock i:nth-of-type(2) {grid-area: y} +.fullclock i:nth-of-type(3) {grid-area: z} +.fullclock > span {grid-area: c} +.fullclock time {padding: 0 0.3em} +.fullclock time b {font-family: OCR A Std, Orator Std, monospace; + font-size: 1.2em} +.fullclock i {font-size: 70%; font-style: normal; color: #9F9} +.fullclock button {width: 100%; + font: 80%/1 Noto Sans Symbols, Symbola, Noto Emoji, sans-serif} +/* The span is made into a pie chart that shows the fraction of time used. */ +.fullclock > span, .clock > span {display: inline-block; + width: 3.5em; height: 3.5em; border-radius: 50%; background: #FFF; + background: conic-gradient( + #000 calc(var(--time-factor) * 360deg), + #FFF calc(var(--time-factor) * 360deg), + #FFF 360deg), #FFF} +@supports not (background: conic-gradient( + #000 calc(var(--time-factor) * 360deg), + #FFF calc(var(--time-factor) * 360deg), + #FFF 360deg), #FFF) { + /* If pie chart not possible, show a clock hand that turns */ + .fullclock > span, .clock > span {position: relative; background: #FFF} + .fullclock > span > span, .clock > span > span {height: 2px; + width: 50%; background: #000; position: absolute; top: calc(50% - 1px); + left: 50%; transform-origin: 0 1px; + transform: rotate(calc(var(--time-factor) * 360deg - 90deg))} +} +.clock {padding: 0.3em; display: grid; justify-items: center; gap: 0.1em; + grid: "a a a a" auto + "c c e f" auto + "c c g h" auto + / 1fr 1fr 1fr 1fr} +.clock time {grid-area: a; padding: 0 0.3em} +.clock .timepause {grid-area: g} +.clock .timedec {grid-area: e} +.clock .timeinc {grid-area: f} +.clock .timereset {grid-area: h} +.clock > span {grid-area: c} +.clock time b {font-family: OCR A Std, Orator Std, monospace; + font-size: 1.2em} +.clock button {width: 100%; + font: 80%/1 Noto Sans Symbols, Symbola, Noto Emoji, sans-serif} + +/* When time is nearly up, make the clock orange. */ +body.time-warning .fullclock, body.time-warning .clock {background: + linear-gradient(hsl(33,100%,37%), hsl(33,90%,42%), hsl(33,100%,36%))} +/* When time is up, make the clock red. */ +body[data-time-factor="100"] .fullclock, +body[data-time-factor="100"] .clock {background: + linear-gradient(hsl(0,100%,47%), hsl(0,90%,55%), hsl(0,100%,46%))} + +/* Make the clock blue when it is paused. */ +body.paused .fullclock, body.paused .clock { + background: linear-gradient(hsl(240,85%,55%), hsl(240,80%,60%), + hsl(240,85%,54%))} +body:not(.paused) .timepause :nth-child(1) {display: none} +body.paused .timepause :nth-child(2) {display: none} +body.paused .timepause {opacity: 0.6} + +/* A div with class=ui generated by b6+, containing play, help and + other buttons. */ +.b6-ui {position: fixed; bottom: 0; left: 0; right: 0; z-index: 1; + background: hsla(205,100%,20%,0.85); color: white; display: flex; + flex-wrap: wrap; + /*padding: 0.2em;*/ gap: 0.5em 0; justify-content: center; + box-shadow: 0 0 4px #111} +.b6-ui button {flex: 7.5em 0.03; background: none; color: inherit; + border: none; padding: 0.5rem; font: inherit; font-size: small} +.b6-ui button:hover {background: hsla(0,0%,0%,0.15)} +@media (min-width: 68em) { + .b6-ui button {padding: 0} + .b6-ui span {display: block; line-height: 1.2; margin: 0.5rem} + .b6-ui span:first-child {font-size: 200%} +} + +/* Style for the popup with the table of contents. */ +.toc {width: 95%; max-width: none; margin: auto auto 0 auto; max-height: 90%; + box-sizing: border-box; overflow: auto; background: var(--toc-bg); + color: var(--toc-fg); padding: 0.5em 0.5em 0.5em 1em} +.toc::backdrop {background: hsla(211,100%,17%,0.5)} +.toc ol {margin: 0.5em 0; columns: 18em; column-rule: thin solid; padding: 0} +.toc li {break-inside: avoid; margin-left: 2em} +.toc li::marker {color: var(--toc-list-marker-fg)} +.toc a {text-decoration: none; color: inherit} +.toc br {display: none} +.toc button {float: right} + +/* Overlay canvas for drawing on a slide with the mouse. */ +.b6-canvas {color: var(--b6-canvas-fg); cursor: alias} + +/* To tell the b6plus.js script that this style sheet has rules that + react to the class darkmode on BODY, set the --has-darkmode + property on BODY to "1". (Set it also on elements with + class=has-darkmode, for older versions of b6+.) */ +body, .has-darkmode {--has-darkmode: 1} + +/* Printing. */ +@page { + margin: 1cm; + @bottom-center {content: counter(page)} +} +@media print { + html {font-size: 10pt} + body {background: none; color: black; margin: 0; columns: 40.889em /*= w */; + column-gap: 4em; column-rule: 0.2pt solid} + .slide {border: 0.2pt solid black; margin: 2em auto; display: block; + overflow: hidden; break-inside: avoid; box-shadow: none} + .comment {background: none; color: black; padding: 0; + columns: 25em; column-rule: thin solid; column-gap: 2em; + widows: 2; orphans: 2; width: auto; height: auto; display: block; + border-radius: 0; overflow: auto; + margin: 2em 1em 2em 0; box-shadow: none} + .slide ~ .comment::before {content: none} + .slide summary::before {content: none} + .slide details {visibility: hidden} + .slide summary {visibility: visible} + [role=region][aria-live=assertive], .b6-ui, .clock, .fullclock {display: none} +} + +/* Output to PDF (trick). + + To output to PDF, print the slides to PDF while selecting a + landscape paper size, e.g. A4 landscape or Letter landscape. + + This style sheet assumes that, when the output is in landscape + mode, the goal is to export one slide per page, without margins, + and omitting the comments between the slides. (On the other hand, + to output multiple slides per page and interleave the comments, + choose a page size in portrait mode.) + + Note: Not all user agents respect the 'size' property to set the + size of the output. If they don't, there will be some margin + to the right and below each slide. Prince respects the property. + E.g, to make myslides.pdf from myslides.html: + + prince --page-size=landscape myslides.html + + W3C team can also use the ",pdfui" tool online. +*/ +@media print and (orientation: landscape) { + html {font-size: 7mm} + .comment, .comment.long {display: none} + .slide {margin: 0; page-break-after: always; box-shadow: none; border: none} + + @page { + size: 286.22mm /*= 7 * w */ 161mm /*= 7 * N */; + margin: 0; + @bottom-center {content: none} + } +} +@media print and (orientation: landscape) and (min-width: 11in) { + /* Letter-size paper */ + html {font-size: 0.26902in /*= 11 / w */} + @page {size: 11in 6.1875in /*= 11 / A */} +} +@media print and (orientation: landscape) and (min-width: 296mm) { + /* A4-size paper, 297 x 210mm */ + html {font-size: 7.2636mm /*= 297 / w */} + @page {size: 297mm 167.06mm /*= 297 / A */} +} diff --git a/presentations/tpac-2025/index.html b/presentations/tpac-2025/index.html new file mode 100644 index 0000000..2212b71 --- /dev/null +++ b/presentations/tpac-2025/index.html @@ -0,0 +1,215 @@ + + + + + + Stronger Together: Super-charging Agentic AI with Accessibility Destinations + + + + + + + + + + +
    + + +
    +

    Super-charging Agentic AI with Accessibility Destinations

    +

    WAI-Adapt Task Force

    +
    + Janina Sajka, W3C Invited Expert
    + Abhinav Kumar, SAP Labs India
    + Lionel Wolberger Ph.D., Level Access +
    +
    +
    Introductory slide for TPAC session.
    + + +
    +

    Outline

    +
      +
    1. Agentic AI Scope with an Example
    2. +
    3. How Current Agents Work and their Limitations
    4. +
    5. Brief introduction of Discoverable Destinations
    6. +
    7. Agents + Discoverable Destinations
    8. +
    9. Architecture & Sample Workflows
    10. +
    11. Discussion
    12. +
    +
    +
    Overview of topics covered in this presentation.
    + + +
    +

    Agentic AI Scope

    +
      +
    • Autonomous systems that plan, reason, and execute tasks
    • +
    • Minimal user input and automate multi-step workflows
    • +
    • Understands natural language and acts on behalf of users
    • +
    +
    +
    Explain concept clearly, emphasize autonomy and assistive potential.
    + + +
    +

    Agentic AI Example: Accessibility statement comparision

    +

    User asks: “Compare accessibility statements across 50 partner websites.”

    +
      +
    • Ideal Agent Workflow:
    • +
        + + + +
      +
    +
    +
    Show real-world impact and time savings.
    + + +
    +

    High Level Agent Workflow for Navigation

    +
      +
    • HTML parsing
    • +
    • Site-specific scripts
    • +
    +
    +
    Highlight brittleness of current approach.
    + + +
    +

    Limitations of Current Agents

    +
      +
    • High maintenance
    • +
    • Brittle
    • +
    • No universal navigation standard
    • +
    +
    +
    Set stage for introducing Discoverable Destinations.
    + + +
    +

    Discoverable Destinations

    +
      +
    • Standardized semantic markers for common pages
    • +
    • Examples: help, contact, log-in, accessibility-statement
    • +
    • Enables predictable navigation
    • +
    +
    +
    Explain concept and its accessibility roots.
    + + +
    +

    Stronger Together? (Agents and Discoverable Destinations)

    +
      +
    • Discover destinations via semantic identifiers
    • +
    • Navigate reliably to key pages
    • +
    • Consistent automation across compliant sites
    • +
    +
    +
    Show synergy between AI and semantic web standards.
    + + +
    +

    Stronger Together? (High Level Architecture & Workflows)

    +
      +
    • LLM: Planning, reasoning, content synthesis
    • +
    • Semantic Tools (based on Adapt's Discoverable Destinations): Discovery, navigation, content retrieval
    • +
    • Complex Actions (like password changes): Combine destinations + APIs + human-in-the-loop
    • +
    +
    +
    Talk about flow in detail.
    + + +
    +

    Sample Workflow & Tools

    +

    Tools, using Adapt Discoverable Destinations, could work as follows:

    +
      +
    1. Retrieve list of discoverable destinations
    2. + +
        + + +
      + + +
    +
    +
    Explain tool roles briefly.
    + + + +
    +

    Discussion

    +
    +

    Are agentic AI and Adapt Discoverable Destinations stronger together?

    +
    +
    + + +
    +

    Join us!

    + +
    + + +
    +

    Appendix

    +

    Optional Slides

    +
    + + + +
    +

    Tool Integration using Model Context Protocol (MCP)

    +
      +
    • MCP Overview: An open protocol enabling AI agents to securely connect with external tools and data sources.
    • + + + +
    +
    + + +
    +

    MCP deployment models : How agents call these tools

    +
      + + +
    +
    + + + +