fb-slides 0.1.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/bin/fb-slides.mjs +3 -2
- package/lib/build.mjs +15 -1
- package/lib/config.mjs +19 -2
- package/lib/dev.mjs +31 -3
- package/lib/render.mjs +10 -0
- package/lib/server.mjs +7 -1
- package/lib/theme.mjs +57 -0
- package/package.json +1 -1
- package/runtime/annotate.js +189 -35
- package/runtime/deck.js +4 -2
- package/runtime/index.html +1 -1
- package/runtime/pointer.js +249 -0
- package/runtime/theme.base.css +177 -4
- package/templates/starter/slides.config.js +12 -0
package/README.md
CHANGED
|
@@ -129,6 +129,8 @@ Note: a bad description is the most common reason a tool never gets used.
|
|
|
129
129
|
| --- | --- |
|
|
130
130
|
| `→` / `Space` | next step (fragments included) |
|
|
131
131
|
| `P` | the pen — draw on the slide, the ink fades on its own |
|
|
132
|
+
| `A` | the arrow — click the tail, then the tip; same colours as the pen |
|
|
133
|
+
| `C` | the pointer — replaces the cursor: a glowing halo, a dart aimed at the centre, or the normal mouse |
|
|
132
134
|
| `Shift` (tap) | the spotlight — drag a rectangle, the rest of the slide dims and blurs |
|
|
133
135
|
| `S` | speaker view — notes, timer, next slide |
|
|
134
136
|
| `Esc` / `O` | overview — the slides as a grid |
|
|
@@ -151,6 +153,8 @@ export default {
|
|
|
151
153
|
decks: 'decks', // folder of .md
|
|
152
154
|
demos: 'demo', // folder behind a bare `<!-- demo: name -->`
|
|
153
155
|
static: ['assets', 'demo'], // served and published; auto-detected when omitted
|
|
156
|
+
revealTheme: 'dracula', // one of reveal.js's own themes; omit for this one
|
|
157
|
+
webfonts: false, // let the reveal themes fetch their Google fonts
|
|
154
158
|
theme: 'theme.css', // loaded after the base theme; auto when it exists
|
|
155
159
|
favicon: 'assets/favicon.png',
|
|
156
160
|
|
|
@@ -193,6 +197,34 @@ rather than replaces. Almost everything hangs off the tokens in `:root`:
|
|
|
193
197
|
}
|
|
194
198
|
```
|
|
195
199
|
|
|
200
|
+
### reveal.js themes
|
|
201
|
+
|
|
202
|
+
The fifteen themes from [revealjs.com/themes](https://revealjs.com/themes/) are all in the
|
|
203
|
+
box — pick one by name and nothing else changes:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
export default { revealTheme: 'dracula' };
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`beige` · `black` · `black-contrast` · `blood` · `dracula` · `league` · `moon` · `night` ·
|
|
210
|
+
`serif` · `simple` · `sky` · `solarized` · `white` · `white-contrast`
|
|
211
|
+
|
|
212
|
+
Only the chosen one is ever loaded — one `<link>`, ~7 KB — and the build copies that file
|
|
213
|
+
and nothing else. Two things happen beyond the link:
|
|
214
|
+
|
|
215
|
+
**It is served from the deck, not from a CDN.** Six of the themes open with
|
|
216
|
+
`@import url(https://fonts.googleapis.com/…)`. Those lines are stripped, so a talk still
|
|
217
|
+
renders with the wifi off; the theme falls back to the next font in its own stack. Pass
|
|
218
|
+
`webfonts: true` to let them through and get the typography of the previews exactly. The
|
|
219
|
+
font folders reveal ships itself — league-gothic, source-sans-pro — are copied into
|
|
220
|
+
`dist/` when the theme asks for them.
|
|
221
|
+
|
|
222
|
+
**The chrome follows the theme.** The navigator, the badge and the signature are not
|
|
223
|
+
`.reveal` elements, so a borrowed theme would leave a black panel down the side of a white
|
|
224
|
+
deck. Every reveal 5 theme declares its palette as `--r-*` custom properties, and the base
|
|
225
|
+
theme points its own tokens at them, so the whole page moves together. A `theme.css` in the
|
|
226
|
+
project still has the last word over both.
|
|
227
|
+
|
|
196
228
|
## Publishing
|
|
197
229
|
|
|
198
230
|
```bash
|
package/bin/fb-slides.mjs
CHANGED
|
@@ -69,11 +69,12 @@ const run = async () => {
|
|
|
69
69
|
|
|
70
70
|
const { loadConfig } = await import('../lib/config.mjs');
|
|
71
71
|
const root = process.cwd();
|
|
72
|
-
const
|
|
72
|
+
const reload = () => loadConfig(root, overrides());
|
|
73
|
+
const config = await reload();
|
|
73
74
|
|
|
74
75
|
if (command === 'dev') {
|
|
75
76
|
const { dev } = await import('../lib/dev.mjs');
|
|
76
|
-
return void (await dev(config, RUNTIME));
|
|
77
|
+
return void (await dev(config, RUNTIME, reload));
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
if (command === 'build') {
|
package/lib/build.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
|
14
14
|
import { assertUsable } from './config.mjs';
|
|
15
15
|
import { listDecks } from './decks.mjs';
|
|
16
16
|
import { renderIndex } from './render.mjs';
|
|
17
|
+
import { REVEAL_THEME_URL, readRevealTheme } from './theme.mjs';
|
|
17
18
|
import { VENDOR_FILES, VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
18
19
|
|
|
19
20
|
const copyFile = async (from, to) => {
|
|
@@ -59,6 +60,18 @@ export const build = async (config, runtimeDir, { quiet = false } = {}) => {
|
|
|
59
60
|
await cp(from, join(out, dir), { recursive: true, filter: (src) => !excluded(src) });
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// 3b. the reveal theme, rewritten, with the font folders it imports — and
|
|
64
|
+
// only those: source-sans-pro is 1.8 MB and most themes never ask for it.
|
|
65
|
+
if (config.revealTheme) {
|
|
66
|
+
const { css, fonts, dir } = await readRevealTheme(config.revealTheme, { webfonts: config.webfonts });
|
|
67
|
+
await writeFile(join(out, REVEAL_THEME_URL), css);
|
|
68
|
+
for (const font of fonts) {
|
|
69
|
+
await cp(join(dir, 'fonts', font), join(out, 'vendor', VENDOR_MOUNTS['reveal.js'], 'dist/theme/fonts', font), {
|
|
70
|
+
recursive: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
// 4. loose root files the config names — a theme override, a favicon.
|
|
63
76
|
for (const file of [config.theme, config.favicon].filter(Boolean)) {
|
|
64
77
|
const from = resolve(config.root, file);
|
|
@@ -73,7 +86,8 @@ export const build = async (config, runtimeDir, { quiet = false } = {}) => {
|
|
|
73
86
|
await writeFile(join(out, 'decks.json'), `${JSON.stringify(decks, null, 2)}\n`);
|
|
74
87
|
|
|
75
88
|
if (!quiet) {
|
|
76
|
-
|
|
89
|
+
const theme = config.revealTheme ? ` · theme ${config.revealTheme}` : '';
|
|
90
|
+
console.log(` ${config.outDir}/ ← ${decks.length} decks · ${config.static.join(', ') || 'no static dirs'}${theme} · ${Date.now() - started}ms`);
|
|
77
91
|
}
|
|
78
92
|
return { decks, out };
|
|
79
93
|
};
|
package/lib/config.mjs
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
// needs running. A project that follows the default shape needs no config at all.
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
9
|
+
import { existsSync, statSync } from 'node:fs';
|
|
10
10
|
import { basename, join, resolve } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
|
|
13
|
+
import { assertRevealTheme } from './theme.mjs';
|
|
14
|
+
|
|
13
15
|
const CONFIG_FILES = ['slides.config.js', 'slides.config.mjs', 'slides.config.json'];
|
|
14
16
|
|
|
15
17
|
// Directories a project gets served and published for free when it has them, so
|
|
@@ -22,7 +24,10 @@ export const ALWAYS_EXCLUDED = ['node_modules', '.git', '.angular', '.next', '.c
|
|
|
22
24
|
export const findConfigFile = (root) => CONFIG_FILES.map((name) => join(root, name)).find(existsSync) ?? null;
|
|
23
25
|
|
|
24
26
|
const importConfig = async (file) => {
|
|
25
|
-
|
|
27
|
+
// ESM caches a module by URL for the life of the process, so the dev server's
|
|
28
|
+
// second read of an edited config would be the first one again. The mtime in
|
|
29
|
+
// the query makes it a different URL exactly when the file has changed.
|
|
30
|
+
const url = `${pathToFileURL(file).href}?t=${statSync(file).mtimeMs}`;
|
|
26
31
|
const module = file.endsWith('.json')
|
|
27
32
|
? await import(url, { with: { type: 'json' } })
|
|
28
33
|
: await import(url);
|
|
@@ -53,6 +58,14 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
53
58
|
decksDir,
|
|
54
59
|
demosDir,
|
|
55
60
|
static: statics,
|
|
61
|
+
// One of reveal's own fifteen — 'dracula', 'sky', 'white'. It lands after
|
|
62
|
+
// the base theme and takes the slides; the chrome follows it through the
|
|
63
|
+
// `--r-*` variables every reveal 5 theme declares. Null: this package's look.
|
|
64
|
+
revealTheme: user.revealTheme ?? null,
|
|
65
|
+
// Six of those themes pull their typefaces from fonts.googleapis.com. Off by
|
|
66
|
+
// default — the deck is served whole or not at all — at the cost of falling
|
|
67
|
+
// back to the theme's second-choice font stack.
|
|
68
|
+
webfonts: user.webfonts ?? false,
|
|
56
69
|
// The project's own CSS, loaded *after* the base theme: an override, not a
|
|
57
70
|
// replacement. `theme.css` at the root is picked up without being declared.
|
|
58
71
|
theme: user.theme ?? (existsSync(join(root, 'theme.css')) ? 'theme.css' : null),
|
|
@@ -72,6 +85,10 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
72
85
|
exclude: [...ALWAYS_EXCLUDED, overrides.outDir ?? user.outDir ?? 'dist', ...(user.exclude ?? [])],
|
|
73
86
|
};
|
|
74
87
|
|
|
88
|
+
// Named early: a typo in the config should be a message, not a deck that
|
|
89
|
+
// silently wears the wrong clothes.
|
|
90
|
+
if (config.revealTheme) await assertRevealTheme(config.revealTheme);
|
|
91
|
+
|
|
75
92
|
config.decksPath = resolve(root, config.decksDir);
|
|
76
93
|
config.outPath = resolve(root, config.outDir);
|
|
77
94
|
// What the browser asks for, as opposed to where it is on disk.
|
package/lib/dev.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { join, resolve } from 'node:path';
|
|
|
13
13
|
import { assertUsable } from './config.mjs';
|
|
14
14
|
import { createDeckServer, listen } from './server.mjs';
|
|
15
15
|
import { renderIndex } from './render.mjs';
|
|
16
|
+
import { REVEAL_THEME_URL, readRevealTheme } from './theme.mjs';
|
|
16
17
|
import { VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
17
18
|
|
|
18
19
|
const OPEN = { darwin: 'open', win32: 'start' };
|
|
@@ -52,8 +53,25 @@ const startSideServer = (spec, root, children) => {
|
|
|
52
53
|
});
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// What the page is built from, read again on every request. A config with a
|
|
57
|
+
// syntax error in it — the state it is in halfway through an edit — leaves the
|
|
58
|
+
// last good one standing rather than serving a broken deck.
|
|
59
|
+
const reader = (config, reload) => {
|
|
60
|
+
let good = config;
|
|
61
|
+
return async () => {
|
|
62
|
+
if (!reload) return good;
|
|
63
|
+
try {
|
|
64
|
+
good = await reload();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn(` ⚠ slides.config: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
return good;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const dev = async (config, runtimeDir, reload) => {
|
|
56
73
|
assertUsable(config);
|
|
74
|
+
const current = reader(config, reload);
|
|
57
75
|
|
|
58
76
|
const mounts = [
|
|
59
77
|
// The project first: a file next to the decks shadows the one this package
|
|
@@ -69,8 +87,18 @@ export const dev = async (config, runtimeDir) => {
|
|
|
69
87
|
const server = createDeckServer({
|
|
70
88
|
mounts,
|
|
71
89
|
decksPath: config.decksPath,
|
|
72
|
-
// Rendered per request
|
|
73
|
-
|
|
90
|
+
// Rendered per request from a config read per request: editing the title,
|
|
91
|
+
// the theme or the signature and reloading is enough. What was fixed when
|
|
92
|
+
// the server came up — the mounts, the port, the side processes — still
|
|
93
|
+
// needs a restart.
|
|
94
|
+
index: async () => renderIndex(await current(), runtimeDir),
|
|
95
|
+
generated: {
|
|
96
|
+
[`/${REVEAL_THEME_URL}`]: async () => {
|
|
97
|
+
const now = await current();
|
|
98
|
+
if (!now.revealTheme) return '/* no revealTheme in slides.config.js */';
|
|
99
|
+
return (await readRevealTheme(now.revealTheme, { webfonts: now.webfonts })).css;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
74
102
|
});
|
|
75
103
|
|
|
76
104
|
await listen(server, config.port);
|
package/lib/render.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
10
|
+
import { REVEAL_THEME_URL } from './theme.mjs';
|
|
11
|
+
|
|
10
12
|
const escapeHtml = (value) =>
|
|
11
13
|
String(value).replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
12
14
|
|
|
@@ -28,6 +30,9 @@ const signatureHtml = (signature) => {
|
|
|
28
30
|
const headHtml = (config) => {
|
|
29
31
|
const tags = [];
|
|
30
32
|
if (config.favicon) tags.push(`<link rel="icon" href="${escapeHtml(config.favicon)}" />`);
|
|
33
|
+
// After the base theme, before the project's: reveal's theme dresses the
|
|
34
|
+
// slides, and whatever the project says still has the last word.
|
|
35
|
+
if (config.revealTheme) tags.push(`<link rel="stylesheet" href="${REVEAL_THEME_URL}" />`);
|
|
31
36
|
// After the base theme on purpose: the project's CSS overrides, it does not replace.
|
|
32
37
|
if (config.theme) tags.push(`<link rel="stylesheet" href="${escapeHtml(config.theme)}" />`);
|
|
33
38
|
return tags.join('\n ');
|
|
@@ -43,7 +48,12 @@ export const renderIndex = async (config, runtimeDir) => {
|
|
|
43
48
|
fragmentLists: config.fragmentLists,
|
|
44
49
|
};
|
|
45
50
|
|
|
51
|
+
// The chrome reads this to take its colours from the reveal theme instead of
|
|
52
|
+
// the base one — see the bridge at the top of theme.base.css.
|
|
53
|
+
const htmlAttrs = config.revealTheme ? ` data-reveal-theme="${escapeHtml(config.revealTheme)}"` : '';
|
|
54
|
+
|
|
46
55
|
return template
|
|
56
|
+
.replaceAll('{{htmlAttrs}}', htmlAttrs)
|
|
47
57
|
.replaceAll('{{lang}}', escapeHtml(config.lang))
|
|
48
58
|
.replaceAll('{{title}}', escapeHtml(config.title))
|
|
49
59
|
.replaceAll('{{head}}', headHtml(config))
|
package/lib/server.mjs
CHANGED
|
@@ -99,7 +99,9 @@ const sendFile = (req, res, path, size) => {
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
// mounts: [{ prefix: '/', dir }] — checked in order, first hit wins.
|
|
102
|
-
|
|
102
|
+
// generated: { '/path.css': async () => body } — files this package rewrites
|
|
103
|
+
// rather than serves, checked before any mount so node_modules cannot shadow one.
|
|
104
|
+
export const createDeckServer = ({ mounts, index, decksPath, generated = {} }) => {
|
|
103
105
|
const handler = async (req, res) => {
|
|
104
106
|
if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, 'method not allowed');
|
|
105
107
|
|
|
@@ -120,6 +122,10 @@ export const createDeckServer = ({ mounts, index, decksPath }) => {
|
|
|
120
122
|
return send(res, 200, body, { 'Content-Type': MIME['.json'] });
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
if (generated[pathname]) {
|
|
126
|
+
return send(res, 200, await generated[pathname](), { 'Content-Type': mime(pathname) });
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
if (DENIED.test(pathname)) return send(res, 403, 'forbidden', { 'Content-Type': MIME['.txt'] });
|
|
124
130
|
|
|
125
131
|
for (const { prefix, dir } of mounts) {
|
package/lib/theme.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// reveal.js ships fifteen themes; a deck wears one of them, or none.
|
|
3
|
+
//
|
|
4
|
+
// They are not linked straight out of node_modules. Six of the fifteen open with
|
|
5
|
+
// `@import url(https://fonts.googleapis.com/…)`, which is a third-party request
|
|
6
|
+
// on every load — and a talk that has to survive the conference wifi cannot rest
|
|
7
|
+
// on one. The file is rewritten instead: the remote imports go, the font folders
|
|
8
|
+
// reveal already ships locally stay, and what the browser gets is served from
|
|
9
|
+
// this project like everything else.
|
|
10
|
+
//
|
|
11
|
+
// The rewritten file is generated per request in dev and written once by build,
|
|
12
|
+
// at the same URL either way — index.html never learns which of the two it is.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
16
|
+
import { basename, join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
19
|
+
|
|
20
|
+
export const REVEAL_THEME_URL = 'reveal-theme.css';
|
|
21
|
+
|
|
22
|
+
const themeDir = () => join(packageDir('reveal.js'), 'dist', 'theme');
|
|
23
|
+
|
|
24
|
+
// The theme is served from the site root, so reveal's own `./fonts/…` would
|
|
25
|
+
// resolve one folder too high: point it at the vendor mount instead.
|
|
26
|
+
const FONT_BASE = `vendor/${VENDOR_MOUNTS['reveal.js']}/dist/theme/fonts/`;
|
|
27
|
+
|
|
28
|
+
const REMOTE_IMPORT = /@import\s+url\(\s*['"]?https?:[^)]*\)\s*;?[ \t]*\n?/gi;
|
|
29
|
+
const LOCAL_FONT_URL = /url\(\s*['"]?\.\/fonts\//gi;
|
|
30
|
+
|
|
31
|
+
export const listRevealThemes = async () =>
|
|
32
|
+
(await readdir(themeDir()))
|
|
33
|
+
.filter((file) => file.endsWith('.css'))
|
|
34
|
+
.map((file) => basename(file, '.css'))
|
|
35
|
+
.sort();
|
|
36
|
+
|
|
37
|
+
export const assertRevealTheme = async (name) => {
|
|
38
|
+
const themes = await listRevealThemes();
|
|
39
|
+
if (themes.includes(name)) return name;
|
|
40
|
+
throw new Error(
|
|
41
|
+
`unknown revealTheme: ${name}\n` +
|
|
42
|
+
` reveal.js ships: ${themes.join(', ')}\n` +
|
|
43
|
+
` drop the key for this package's own theme`,
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// The font folders the chosen theme actually imports. build copies these and
|
|
48
|
+
// nothing else: source-sans-pro alone is 1.8 MB, and most themes never ask for it.
|
|
49
|
+
const fontDirs = (css) => [...new Set([...css.matchAll(/\.\/fonts\/([^/]+)\//g)].map((m) => m[1]))];
|
|
50
|
+
|
|
51
|
+
// `webfonts: true` keeps the Google Fonts imports — the exact typography of
|
|
52
|
+
// revealjs.com/themes, at the cost of the offline guarantee.
|
|
53
|
+
export const readRevealTheme = async (name, { webfonts = false } = {}) => {
|
|
54
|
+
const source = await readFile(join(themeDir(), `${name}.css`), 'utf8');
|
|
55
|
+
const css = (webfonts ? source : source.replace(REMOTE_IMPORT, '')).replace(LOCAL_FONT_URL, `url(${FONT_BASE}`);
|
|
56
|
+
return { css, fonts: fontDirs(source), dir: themeDir() };
|
|
57
|
+
};
|
package/package.json
CHANGED
package/runtime/annotate.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
// whole feature — its chrome, its canvas and its key bindings — and index.html
|
|
6
6
|
// stays a page of slides. Register it in the `plugins:` array like any other.
|
|
7
7
|
//
|
|
8
|
+
// Two tools share everything here: the pen, and the arrow — click where it
|
|
9
|
+
// starts, click (or release a drag) where it points. Same canvas, same
|
|
10
|
+
// palette, same hold-then-fade; an arrow is just one more kind of stroke.
|
|
11
|
+
//
|
|
8
12
|
// The ink goes on one full-viewport canvas in screen pixels, which keeps it
|
|
9
13
|
// clear of the CSS transform reveal scales a slide with: the pen follows the
|
|
10
14
|
// pointer, not the slide's coordinate space.
|
|
@@ -25,9 +29,11 @@ const COLOURS = [
|
|
|
25
29
|
{ label: 'Black', glow: '#000000', core: '#000000' },
|
|
26
30
|
];
|
|
27
31
|
|
|
28
|
-
// A released stroke rubs itself out from its
|
|
29
|
-
// FADE_MS is how long
|
|
30
|
-
// the erase front softens as it passes —
|
|
32
|
+
// A released stroke holds on screen for HOLD_MS, then rubs itself out from its
|
|
33
|
+
// oldest point towards its newest. FADE_MS is how long the erase takes end to
|
|
34
|
+
// end, FADE_TAIL how much of the stroke the erase front softens as it passes —
|
|
35
|
+
// at 0 it would be a hard wipe.
|
|
36
|
+
const HOLD_MS = 2000;
|
|
31
37
|
const FADE_MS = 1600;
|
|
32
38
|
const FADE_TAIL = 0.35;
|
|
33
39
|
|
|
@@ -44,6 +50,14 @@ const TENSION = 1 / 6;
|
|
|
44
50
|
|
|
45
51
|
const WIDTH = 3.4; // px, the bright core of the line
|
|
46
52
|
|
|
53
|
+
// The arrow's head: how far back along the shaft the wings reach, and how far
|
|
54
|
+
// they open off it. A short arrow shrinks its head rather than being all head.
|
|
55
|
+
const HEAD_LEN = 48;
|
|
56
|
+
const HEAD_SPREAD = 0.55; // radians
|
|
57
|
+
// Under this, a press-and-release is a click placing the tail, not a whole
|
|
58
|
+
// arrow: the tip then follows the pointer until the second click.
|
|
59
|
+
const MIN_ARROW = 12;
|
|
60
|
+
|
|
47
61
|
// Two layers, each the same path stroked at a few widths: a soft halo, then the
|
|
48
62
|
// core on top of it. Within a layer the passes stack additively, hence the low
|
|
49
63
|
// alphas. Cheaper than a shadowBlur per segment.
|
|
@@ -62,8 +76,10 @@ const QUANTA = 24;
|
|
|
62
76
|
// `P` is reveal's own previous-slide key, and a plugin binding wins over it —
|
|
63
77
|
// which is the point: on stage the pen is reached for far more often than a key
|
|
64
78
|
// ← and Shift+Space already do. Reveal's own row in the help overlay still
|
|
65
|
-
// lists it, though, so it has to be corrected.
|
|
66
|
-
|
|
79
|
+
// lists it, though, so it has to be corrected. `A` is reveal's auto-slide
|
|
80
|
+
// toggle, idle in a deck that does not auto-advance — the arrow takes it.
|
|
81
|
+
const PEN_KEY_CODE = 80;
|
|
82
|
+
const ARROW_KEY_CODE = 65;
|
|
67
83
|
const HELP_TAKEN_FROM = { key: 'P', from: /previous/i };
|
|
68
84
|
|
|
69
85
|
const PEN_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
@@ -71,6 +87,11 @@ const PEN_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" str
|
|
|
71
87
|
<path d="M15.2 4.6l4.2 4.2" /><path d="M4 20l4.7-1L19.8 7.9a2 2 0 0 0-2.8-2.8L5.9 16.3 4 20z" />
|
|
72
88
|
</svg>`;
|
|
73
89
|
|
|
90
|
+
const ARROW_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
91
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
92
|
+
<path d="M5 19L19 5" /><path d="M9.5 5H19v9.5" />
|
|
93
|
+
</svg>`;
|
|
94
|
+
|
|
74
95
|
// Black text on a light chip, light text on a dark one — the armed pen button
|
|
75
96
|
// wears the ink's own colour, and two of the six are the extremes.
|
|
76
97
|
const readable = (hex) => {
|
|
@@ -124,7 +145,9 @@ const RevealAnnotate = () => ({
|
|
|
124
145
|
toolbar.innerHTML = `
|
|
125
146
|
<button id="tool-pen" class="deck-tool" type="button" aria-pressed="false"
|
|
126
147
|
title="Pen — draw on the slide (P)" aria-label="Pen">${PEN_ICON}</button>
|
|
127
|
-
<
|
|
148
|
+
<button id="tool-arrow" class="deck-tool" type="button" aria-pressed="false"
|
|
149
|
+
title="Arrow — click the tail, then the tip (A)" aria-label="Arrow">${ARROW_ICON}</button>
|
|
150
|
+
<div id="deck-swatches" role="radiogroup" aria-label="Ink colour" hidden>
|
|
128
151
|
${COLOURS.map(
|
|
129
152
|
(colour, i) => `<button class="deck-swatch" type="button" role="radio"
|
|
130
153
|
aria-checked="${i === 0}" data-colour="${i}" style="--ink:${colour.glow}"
|
|
@@ -135,6 +158,7 @@ const RevealAnnotate = () => ({
|
|
|
135
158
|
|
|
136
159
|
document.body.append(canvas, toolbar);
|
|
137
160
|
const penButton = toolbar.querySelector('#tool-pen');
|
|
161
|
+
const arrowButton = toolbar.querySelector('#tool-arrow');
|
|
138
162
|
const swatches = toolbar.querySelector('#deck-swatches');
|
|
139
163
|
const ctx = canvas.getContext('2d');
|
|
140
164
|
|
|
@@ -206,10 +230,14 @@ const RevealAnnotate = () => ({
|
|
|
206
230
|
// ---- strokes --------------------------------------------------------
|
|
207
231
|
// A stroke is its points, the colour it was drawn in, and the moment the
|
|
208
232
|
// pointer left the slide: `releasedAt: null` is one still under the pen.
|
|
233
|
+
// An arrow rides the same list with `kind: 'arrow'` and two points, `from`
|
|
234
|
+
// and `to` — it holds and fades exactly like a line of ink.
|
|
209
235
|
|
|
210
236
|
const strokes = [];
|
|
211
|
-
let drawing = null;
|
|
237
|
+
let drawing = null; // the pen's live stroke, or null
|
|
238
|
+
let placing = null; // an arrow whose tip still follows the pointer, or null
|
|
212
239
|
let frame = 0;
|
|
240
|
+
let holdTimer = 0;
|
|
213
241
|
|
|
214
242
|
const addPoint = (event) => {
|
|
215
243
|
const points = drawing.points;
|
|
@@ -282,11 +310,65 @@ const RevealAnnotate = () => ({
|
|
|
282
310
|
return runs;
|
|
283
311
|
};
|
|
284
312
|
|
|
313
|
+
// An arrow has no oldest end worth erasing first: it fades out whole, the
|
|
314
|
+
// shaft and its head together. The shaft ends where the head begins, so
|
|
315
|
+
// the glow's rounded cap never pokes past the tip.
|
|
316
|
+
const paintArrow = (stroke, progress) => {
|
|
317
|
+
const { from, to } = stroke;
|
|
318
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
319
|
+
if (length < 1) return true;
|
|
320
|
+
const box = boundsOf([from, to]);
|
|
321
|
+
if (box.w <= 0 || box.h <= 0) return true;
|
|
322
|
+
|
|
323
|
+
const fade = progress === null ? 1 : 1 - progress;
|
|
324
|
+
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
325
|
+
const head = Math.min(HEAD_LEN, length * 0.6);
|
|
326
|
+
const wings = [HEAD_SPREAD, -HEAD_SPREAD].map((spread) => ({
|
|
327
|
+
x: to.x - head * Math.cos(angle + spread),
|
|
328
|
+
y: to.y - head * Math.sin(angle + spread),
|
|
329
|
+
}));
|
|
330
|
+
const base = {
|
|
331
|
+
x: to.x - head * 0.75 * Math.cos(angle),
|
|
332
|
+
y: to.y - head * 0.75 * Math.sin(angle),
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
for (const { ink, passes } of LAYERS) {
|
|
336
|
+
onLayer(box, stroke.ink[ink], (c) => {
|
|
337
|
+
c.lineCap = 'round';
|
|
338
|
+
for (const pass of passes) {
|
|
339
|
+
c.lineWidth = WIDTH * pass.width;
|
|
340
|
+
c.strokeStyle = c.fillStyle = `rgba(255, 255, 255, ${fade * pass.alpha})`;
|
|
341
|
+
c.beginPath();
|
|
342
|
+
c.moveTo(from.x, from.y);
|
|
343
|
+
c.lineTo(base.x, base.y);
|
|
344
|
+
c.stroke();
|
|
345
|
+
// The head is filled for its body and stroked for its glow: the
|
|
346
|
+
// stroke fattens it by the same width the shaft wears.
|
|
347
|
+
c.beginPath();
|
|
348
|
+
c.moveTo(to.x, to.y);
|
|
349
|
+
c.lineTo(wings[0].x, wings[0].y);
|
|
350
|
+
c.lineTo(wings[1].x, wings[1].y);
|
|
351
|
+
c.closePath();
|
|
352
|
+
c.fill();
|
|
353
|
+
c.stroke();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
};
|
|
359
|
+
|
|
285
360
|
// Returns false once the stroke has faded out and can be dropped.
|
|
286
361
|
const paint = (stroke, now) => {
|
|
287
|
-
|
|
362
|
+
// Clamped at 0 through the hold: a stroke waiting its turn paints exactly
|
|
363
|
+
// like one still under the pen, only released.
|
|
364
|
+
const progress =
|
|
365
|
+
stroke.releasedAt === null
|
|
366
|
+
? null
|
|
367
|
+
: Math.max(0, (now - stroke.releasedAt - HOLD_MS) / FADE_MS);
|
|
288
368
|
if (progress !== null && progress >= 1) return false;
|
|
289
369
|
|
|
370
|
+
if (stroke.kind === 'arrow') return paintArrow(stroke, progress);
|
|
371
|
+
|
|
290
372
|
const box = boundsOf(stroke.points);
|
|
291
373
|
if (box.w <= 0 || box.h <= 0) return true;
|
|
292
374
|
|
|
@@ -334,25 +416,36 @@ const RevealAnnotate = () => ({
|
|
|
334
416
|
if (!paint(strokes[i], now)) strokes.splice(i, 1);
|
|
335
417
|
}
|
|
336
418
|
// Only a fading stroke needs the next frame. A live one changes when a
|
|
337
|
-
// point is added and not before,
|
|
338
|
-
//
|
|
339
|
-
|
|
419
|
+
// point is added and not before, and a released one sits untouched
|
|
420
|
+
// through its hold — for those a single timeout wakes the loop when the
|
|
421
|
+
// first fade is due, instead of repainting a full-screen canvas forever.
|
|
422
|
+
const waits = strokes
|
|
423
|
+
.filter((stroke) => stroke.releasedAt !== null)
|
|
424
|
+
.map((stroke) => stroke.releasedAt + HOLD_MS - now);
|
|
425
|
+
if (waits.some((wait) => wait <= 0)) schedule();
|
|
426
|
+
else if (waits.length) holdTimer = setTimeout(schedule, Math.min(...waits));
|
|
340
427
|
};
|
|
341
428
|
|
|
342
429
|
const schedule = () => {
|
|
430
|
+
clearTimeout(holdTimer);
|
|
431
|
+
holdTimer = 0;
|
|
343
432
|
frame ||= requestAnimationFrame(render);
|
|
344
433
|
};
|
|
345
434
|
|
|
346
435
|
const clear = () => {
|
|
347
436
|
drawing = null;
|
|
437
|
+
placing = null;
|
|
348
438
|
strokes.length = 0;
|
|
439
|
+
clearTimeout(holdTimer);
|
|
440
|
+
holdTimer = 0;
|
|
349
441
|
ctx.clearRect(0, 0, width, height);
|
|
350
442
|
};
|
|
351
443
|
|
|
352
|
-
// ---- the
|
|
444
|
+
// ---- the tools ------------------------------------------------------
|
|
353
445
|
|
|
354
|
-
let
|
|
446
|
+
let tool = null; // null, 'pen' or 'arrow'
|
|
355
447
|
let ink = COLOURS[0];
|
|
448
|
+
const toolButtons = { pen: penButton, arrow: arrowButton };
|
|
356
449
|
|
|
357
450
|
const setInk = (colour) => {
|
|
358
451
|
ink = colour;
|
|
@@ -364,29 +457,60 @@ const RevealAnnotate = () => ({
|
|
|
364
457
|
}
|
|
365
458
|
};
|
|
366
459
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
460
|
+
const setTool = (next) => {
|
|
461
|
+
if (next === tool) return;
|
|
462
|
+
// Whatever was mid-gesture under the old tool does not survive the
|
|
463
|
+
// switch: a half-drawn line is released, a half-aimed arrow discarded.
|
|
464
|
+
if (drawing) {
|
|
465
|
+
drawing.releasedAt = performance.now();
|
|
466
|
+
drawing = null;
|
|
467
|
+
}
|
|
468
|
+
if (placing) {
|
|
469
|
+
strokes.splice(strokes.indexOf(placing), 1);
|
|
470
|
+
placing = null;
|
|
471
|
+
schedule();
|
|
472
|
+
}
|
|
473
|
+
tool = next;
|
|
474
|
+
for (const [name, button] of Object.entries(toolButtons)) {
|
|
475
|
+
button.setAttribute('aria-pressed', String(name === tool));
|
|
476
|
+
}
|
|
477
|
+
toolbar.classList.toggle('is-armed', tool !== null);
|
|
478
|
+
// The colours are only worth showing while there is a tool to apply them
|
|
479
|
+
// to, which makes arming one the gesture that opens them.
|
|
480
|
+
swatches.hidden = tool === null;
|
|
374
481
|
// Only an armed canvas takes the pointer; the rest of the time clicks
|
|
375
482
|
// fall through to the slide underneath.
|
|
376
|
-
canvas.classList.toggle('is-live',
|
|
377
|
-
if (
|
|
483
|
+
canvas.classList.toggle('is-live', tool !== null);
|
|
484
|
+
if (tool) document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: tool }));
|
|
378
485
|
else clear();
|
|
379
486
|
};
|
|
487
|
+
const toggleTool = (name) => setTool(tool === name ? null : name);
|
|
380
488
|
|
|
381
|
-
//
|
|
489
|
+
// Several tools, one pointer: arming any disarms the rest. The handshake
|
|
382
490
|
// is a DOM event rather than an import either way round, so each plugin
|
|
383
|
-
// works alone.
|
|
491
|
+
// works alone. Pen and arrow are both this plugin's, so only a foreign
|
|
492
|
+
// detail stands it down.
|
|
384
493
|
document.addEventListener('deck:tool-armed', (event) => {
|
|
385
|
-
if (event.detail !== 'pen' &&
|
|
494
|
+
if (event.detail !== 'pen' && event.detail !== 'arrow' && tool) setTool(null);
|
|
386
495
|
});
|
|
387
496
|
|
|
388
497
|
canvas.addEventListener('pointerdown', (event) => {
|
|
389
|
-
if (!
|
|
498
|
+
if (!tool || event.button !== 0) return;
|
|
499
|
+
if (tool === 'arrow') {
|
|
500
|
+
// Second click: the tip lands here. First click: the tail does, and
|
|
501
|
+
// the tip follows the pointer until the next one.
|
|
502
|
+
if (placing) {
|
|
503
|
+
placing.to = { x: event.clientX, y: event.clientY };
|
|
504
|
+
placing.releasedAt = performance.now();
|
|
505
|
+
placing = null;
|
|
506
|
+
} else {
|
|
507
|
+
const at = { x: event.clientX, y: event.clientY };
|
|
508
|
+
placing = { kind: 'arrow', from: at, to: { ...at }, releasedAt: null, ink };
|
|
509
|
+
strokes.push(placing);
|
|
510
|
+
}
|
|
511
|
+
schedule();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
390
514
|
canvas.setPointerCapture(event.pointerId);
|
|
391
515
|
// The colour is caught at the down-stroke, so a line already fading keeps
|
|
392
516
|
// the one it was drawn in when you pick another.
|
|
@@ -397,6 +521,12 @@ const RevealAnnotate = () => ({
|
|
|
397
521
|
});
|
|
398
522
|
|
|
399
523
|
canvas.addEventListener('pointermove', (event) => {
|
|
524
|
+
// An arrow being aimed follows the bare pointer — no button held.
|
|
525
|
+
if (placing) {
|
|
526
|
+
placing.to = { x: event.clientX, y: event.clientY };
|
|
527
|
+
schedule();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
400
530
|
if (!drawing || event.pointerId !== drawing.id) return;
|
|
401
531
|
// A trackpad reports faster than the display refreshes, and the samples
|
|
402
532
|
// in between are what keeps a quick curve smooth instead of faceted.
|
|
@@ -408,6 +538,19 @@ const RevealAnnotate = () => ({
|
|
|
408
538
|
});
|
|
409
539
|
|
|
410
540
|
const release = (event) => {
|
|
541
|
+
// A drag long enough to be deliberate is the whole gesture: down at the
|
|
542
|
+
// tail, up at the tip. Anything shorter was a click, and the arrow stays
|
|
543
|
+
// under the pointer waiting for the second one.
|
|
544
|
+
if (placing) {
|
|
545
|
+
const span = Math.hypot(event.clientX - placing.from.x, event.clientY - placing.from.y);
|
|
546
|
+
if (span >= MIN_ARROW) {
|
|
547
|
+
placing.to = { x: event.clientX, y: event.clientY };
|
|
548
|
+
placing.releasedAt = performance.now();
|
|
549
|
+
placing = null;
|
|
550
|
+
schedule();
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
411
554
|
if (!drawing || event.pointerId !== drawing.id) return;
|
|
412
555
|
drawing.releasedAt = performance.now();
|
|
413
556
|
drawing = null;
|
|
@@ -416,7 +559,8 @@ const RevealAnnotate = () => ({
|
|
|
416
559
|
canvas.addEventListener('pointerup', release);
|
|
417
560
|
canvas.addEventListener('pointercancel', release);
|
|
418
561
|
|
|
419
|
-
penButton.addEventListener('click', () =>
|
|
562
|
+
penButton.addEventListener('click', () => toggleTool('pen'));
|
|
563
|
+
arrowButton.addEventListener('click', () => toggleTool('arrow'));
|
|
420
564
|
swatches.addEventListener('click', (event) => {
|
|
421
565
|
const button = event.target.closest('.deck-swatch');
|
|
422
566
|
if (button) setInk(COLOURS[button.dataset.colour]);
|
|
@@ -424,17 +568,27 @@ const RevealAnnotate = () => ({
|
|
|
424
568
|
|
|
425
569
|
// `addKeyBinding` with a descriptor binds the key and lists it in reveal's
|
|
426
570
|
// own help overlay, which `registerKeyboardShortcut` alone would not do.
|
|
427
|
-
deck.addKeyBinding({ keyCode:
|
|
428
|
-
|
|
571
|
+
deck.addKeyBinding({ keyCode: PEN_KEY_CODE, key: 'P', description: 'Toggle the pen' }, () =>
|
|
572
|
+
toggleTool('pen'),
|
|
573
|
+
);
|
|
574
|
+
deck.addKeyBinding({ keyCode: ARROW_KEY_CODE, key: 'A', description: 'Toggle the arrow' }, () =>
|
|
575
|
+
toggleTool('arrow'),
|
|
429
576
|
);
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
577
|
+
// Keys the tools borrow only while one is armed, taken in capture phase so
|
|
578
|
+
// reveal's own bindings never see them: Escape (otherwise the overview
|
|
579
|
+
// toggle) disarms it, and the number row picks the ink — 1 to 6, in the
|
|
580
|
+
// order the swatches sit in the toolbar. The rest of the time both fall
|
|
581
|
+
// through untouched.
|
|
433
582
|
document.addEventListener(
|
|
434
583
|
'keydown',
|
|
435
584
|
(event) => {
|
|
436
|
-
if (event.
|
|
437
|
-
|
|
585
|
+
if (!tool || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
586
|
+
if (event.key === 'Escape') setTool(null);
|
|
587
|
+
else {
|
|
588
|
+
const index = '123456'.indexOf(event.key);
|
|
589
|
+
if (index === -1 || index >= COLOURS.length) return;
|
|
590
|
+
setInk(COLOURS[index]);
|
|
591
|
+
}
|
|
438
592
|
event.stopPropagation();
|
|
439
593
|
event.preventDefault();
|
|
440
594
|
},
|
|
@@ -447,7 +601,7 @@ const RevealAnnotate = () => ({
|
|
|
447
601
|
// A new slide is a clean sheet — including a line still under the pointer.
|
|
448
602
|
deck.on('slidechanged', clear);
|
|
449
603
|
// The overview is a different surface; drawing over it would annotate nothing.
|
|
450
|
-
deck.on('overviewshown', () =>
|
|
604
|
+
deck.on('overviewshown', () => setTool(null));
|
|
451
605
|
|
|
452
606
|
window.addEventListener('resize', resize);
|
|
453
607
|
setInk(ink);
|
package/runtime/deck.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import RevealAnnotate from './annotate.js';
|
|
2
2
|
import RevealSpotlight from './spotlight.js';
|
|
3
|
+
import RevealPointer from './pointer.js';
|
|
3
4
|
import RevealOutline from './outline.js';
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -161,8 +162,9 @@ await Reveal.initialize({
|
|
|
161
162
|
pdfSeparateFragments: false,
|
|
162
163
|
// Anything the project wants to change, from `reveal:` in slides.config.js.
|
|
163
164
|
...(CFG.reveal ?? {}),
|
|
164
|
-
// Spotlight after Annotate:
|
|
165
|
-
|
|
165
|
+
// Spotlight and Pointer after Annotate: they hang their buttons on the
|
|
166
|
+
// toolbar the pen builds.
|
|
167
|
+
plugins: [RevealMarkdown, RevealHighlight, RevealNotes, RevealAnnotate(), RevealSpotlight(), RevealPointer(), RevealOutline()],
|
|
166
168
|
});
|
|
167
169
|
|
|
168
170
|
// ---------------------------------------------------------------------------
|
package/runtime/index.html
CHANGED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// RevealPointer — swap the mouse cursor for a stage pointer.
|
|
3
|
+
//
|
|
4
|
+
// A reveal.js plugin, built like spotlight.js: `init()` gets the deck, the file
|
|
5
|
+
// owns its chrome and key bindings, and index.html stays a page of slides.
|
|
6
|
+
//
|
|
7
|
+
// Armed, it hides the real cursor everywhere and hangs its own on the pointer
|
|
8
|
+
// instead. Three kinds, picked from a popover like the pen's swatches:
|
|
9
|
+
//
|
|
10
|
+
// halo — a bright dot inside a glowing ring; the ring trails the dot on a
|
|
11
|
+
// short elastic, which is what makes it read as alive rather than
|
|
12
|
+
// as a cursor theme.
|
|
13
|
+
// compass — a dart that always aims at the centre of the slide, so wherever
|
|
14
|
+
// the hand wanders the audience is pointed back at the deck.
|
|
15
|
+
// native — the system cursor again, without standing the tool down: a way
|
|
16
|
+
// back to a normal mouse that keeps the popover one keystroke away.
|
|
17
|
+
//
|
|
18
|
+
// Unlike the pen and the spotlight it survives a slide change: it is the
|
|
19
|
+
// cursor now, not a mark on one slide.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const KEY_CODE = 67; // C
|
|
23
|
+
|
|
24
|
+
const KINDS = [
|
|
25
|
+
{
|
|
26
|
+
id: 'halo',
|
|
27
|
+
label: 'Halo pointer',
|
|
28
|
+
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
29
|
+
aria-hidden="true"><circle cx="12" cy="12" r="7.5" /><circle cx="12" cy="12" r="1.6"
|
|
30
|
+
fill="currentColor" stroke="none" /></svg>`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'compass',
|
|
34
|
+
label: 'Compass pointer — aims at the centre',
|
|
35
|
+
icon: `<svg viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true">
|
|
36
|
+
<path d="M21 12L4.5 19.5 8.6 12 4.5 4.5z" /></svg>`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'native',
|
|
40
|
+
label: 'Normal cursor',
|
|
41
|
+
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
42
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
43
|
+
<path d="M4 4l7.07 17 2.51-7.39L21 11.07z" /></svg>`,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// How hard the halo's ring is pulled towards the dot each frame: 1 would pin
|
|
48
|
+
// it, lower trails further behind. The loop stops once it has caught up.
|
|
49
|
+
const CHASE = 0.28;
|
|
50
|
+
const SETTLED = 0.4; // px of remaining lag that counts as caught up
|
|
51
|
+
|
|
52
|
+
const POINTER_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
53
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
54
|
+
<circle cx="12" cy="12" r="7" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3" />
|
|
55
|
+
</svg>`;
|
|
56
|
+
|
|
57
|
+
const RevealPointer = () => ({
|
|
58
|
+
id: 'pointer',
|
|
59
|
+
|
|
60
|
+
init(deck) {
|
|
61
|
+
// ---- chrome ---------------------------------------------------------
|
|
62
|
+
// Fixed overlays on <body>, outside `.reveal` and its transforms. The
|
|
63
|
+
// pointer rides above every other overlay — it stands in for the cursor,
|
|
64
|
+
// which is always on top of what it points at.
|
|
65
|
+
const layer = document.createElement('div');
|
|
66
|
+
layer.id = 'deck-pointer';
|
|
67
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
68
|
+
layer.dataset.kind = KINDS[0].id;
|
|
69
|
+
layer.innerHTML = `
|
|
70
|
+
<div class="deck-pointer-halo"></div>
|
|
71
|
+
<div class="deck-pointer-dot"></div>
|
|
72
|
+
<svg class="deck-pointer-compass" viewBox="0 0 24 24" aria-hidden="true">
|
|
73
|
+
<path d="M21 12L4.5 19.5 8.6 12 4.5 4.5z" />
|
|
74
|
+
</svg>
|
|
75
|
+
`;
|
|
76
|
+
layer.hidden = true;
|
|
77
|
+
document.body.append(layer);
|
|
78
|
+
const halo = layer.querySelector('.deck-pointer-halo');
|
|
79
|
+
const dot = layer.querySelector('.deck-pointer-dot');
|
|
80
|
+
const compass = layer.querySelector('.deck-pointer-compass');
|
|
81
|
+
|
|
82
|
+
// The toolbar is annotate's; this plugin registers after it in deck.js and
|
|
83
|
+
// hangs its button there. A deck running without the pen still gets one.
|
|
84
|
+
let toolbar = document.querySelector('#deck-tools');
|
|
85
|
+
if (!toolbar) {
|
|
86
|
+
toolbar = document.createElement('div');
|
|
87
|
+
toolbar.id = 'deck-tools';
|
|
88
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
89
|
+
toolbar.setAttribute('aria-label', 'Slide tools');
|
|
90
|
+
document.body.append(toolbar);
|
|
91
|
+
}
|
|
92
|
+
const button = document.createElement('button');
|
|
93
|
+
button.id = 'tool-pointer';
|
|
94
|
+
button.className = 'deck-tool';
|
|
95
|
+
button.type = 'button';
|
|
96
|
+
button.setAttribute('aria-pressed', 'false');
|
|
97
|
+
button.title = 'Pointer — replace the cursor (C)';
|
|
98
|
+
button.setAttribute('aria-label', 'Pointer');
|
|
99
|
+
button.innerHTML = POINTER_ICON;
|
|
100
|
+
toolbar.append(button);
|
|
101
|
+
|
|
102
|
+
// The kind picker, hanging under the toolbar exactly like the pen's
|
|
103
|
+
// swatches — the two are never open at once, since arming one tool
|
|
104
|
+
// disarms the other.
|
|
105
|
+
const picker = document.createElement('div');
|
|
106
|
+
picker.id = 'deck-pointer-kinds';
|
|
107
|
+
picker.setAttribute('role', 'radiogroup');
|
|
108
|
+
picker.setAttribute('aria-label', 'Pointer kind');
|
|
109
|
+
picker.hidden = true;
|
|
110
|
+
picker.innerHTML = KINDS.map(
|
|
111
|
+
(kind, i) => `<button class="deck-pointer-kind" type="button" role="radio"
|
|
112
|
+
aria-checked="${i === 0}" data-kind="${kind.id}"
|
|
113
|
+
title="${kind.label}" aria-label="${kind.label}">${kind.icon}</button>`,
|
|
114
|
+
).join('');
|
|
115
|
+
toolbar.append(picker);
|
|
116
|
+
|
|
117
|
+
// ---- following the pointer ------------------------------------------
|
|
118
|
+
|
|
119
|
+
let armed = false;
|
|
120
|
+
let kind = KINDS[0].id;
|
|
121
|
+
let x = -100; // the pointer, in screen px; parked off-screen until a move
|
|
122
|
+
let y = -100;
|
|
123
|
+
let hx = -100; // where the halo's ring has got to on its way there
|
|
124
|
+
let hy = -100;
|
|
125
|
+
let frame = 0;
|
|
126
|
+
|
|
127
|
+
const aim = () => {
|
|
128
|
+
// The dart pivots on its own centre to face the middle of the viewport;
|
|
129
|
+
// right at the centre it just keeps its last heading.
|
|
130
|
+
const dx = window.innerWidth / 2 - x;
|
|
131
|
+
const dy = window.innerHeight / 2 - y;
|
|
132
|
+
if (dx || dy) compass.style.rotate = `${Math.atan2(dy, dx)}rad`;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const render = () => {
|
|
136
|
+
frame = 0;
|
|
137
|
+
hx += (x - hx) * CHASE;
|
|
138
|
+
hy += (y - hy) * CHASE;
|
|
139
|
+
// `translate` the individual property, not `transform`: the press effect
|
|
140
|
+
// scales through `transform` in CSS, and the two must not fight.
|
|
141
|
+
dot.style.translate = `${x}px ${y}px`;
|
|
142
|
+
compass.style.translate = `${x}px ${y}px`;
|
|
143
|
+
halo.style.translate = `${hx}px ${hy}px`;
|
|
144
|
+
if (Math.hypot(x - hx, y - hy) > SETTLED) schedule();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const schedule = () => {
|
|
148
|
+
frame ||= requestAnimationFrame(render);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const move = (event) => {
|
|
152
|
+
x = event.clientX;
|
|
153
|
+
y = event.clientY;
|
|
154
|
+
if (!armed || kind === 'native') return;
|
|
155
|
+
layer.hidden = false;
|
|
156
|
+
aim();
|
|
157
|
+
schedule();
|
|
158
|
+
};
|
|
159
|
+
document.addEventListener('pointermove', move);
|
|
160
|
+
// Falling off the window edge — or the window losing focus — leaves no
|
|
161
|
+
// cursor to stand in for; a stale pointer would sit there pointing at
|
|
162
|
+
// nothing until the mouse came back.
|
|
163
|
+
document.addEventListener('mouseleave', () => { layer.hidden = true; });
|
|
164
|
+
window.addEventListener('blur', () => { layer.hidden = true; });
|
|
165
|
+
|
|
166
|
+
// A click lands somewhere: the pointer acknowledges it — the dot swells,
|
|
167
|
+
// the ring snaps in, the dart lunges. Pure CSS off this class.
|
|
168
|
+
document.addEventListener('pointerdown', (event) => {
|
|
169
|
+
if (armed && event.button === 0) layer.classList.add('is-pressed');
|
|
170
|
+
});
|
|
171
|
+
document.addEventListener('pointerup', () => layer.classList.remove('is-pressed'));
|
|
172
|
+
|
|
173
|
+
// ---- arming ---------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
// The real cursor stands down only while a stand-in is up: armed on the
|
|
176
|
+
// native kind, the mouse is its ordinary self and the overlay stays away.
|
|
177
|
+
const applyCursor = () => {
|
|
178
|
+
const standIn = armed && kind !== 'native';
|
|
179
|
+
document.documentElement.classList.toggle('deck-pointer-on', standIn);
|
|
180
|
+
layer.hidden = true; // shown again by the first move, in the right place
|
|
181
|
+
if (standIn) {
|
|
182
|
+
// Start the ring already caught up, not sprinting in from off-screen.
|
|
183
|
+
hx = x;
|
|
184
|
+
hy = y;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const setKind = (next) => {
|
|
189
|
+
kind = next;
|
|
190
|
+
layer.dataset.kind = kind;
|
|
191
|
+
for (const chip of picker.children) {
|
|
192
|
+
chip.setAttribute('aria-checked', String(chip.dataset.kind === kind));
|
|
193
|
+
}
|
|
194
|
+
applyCursor();
|
|
195
|
+
aim();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const setPointer = (on) => {
|
|
199
|
+
armed = on;
|
|
200
|
+
button.setAttribute('aria-pressed', String(on));
|
|
201
|
+
toolbar.classList.toggle('is-armed', on);
|
|
202
|
+
picker.hidden = !on;
|
|
203
|
+
applyCursor();
|
|
204
|
+
if (on) document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'pointer' }));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Several tools, one pointer: arming any disarms the rest — same DOM-event
|
|
208
|
+
// handshake as the pen and the spotlight, so each plugin works alone.
|
|
209
|
+
document.addEventListener('deck:tool-armed', (event) => {
|
|
210
|
+
if (event.detail !== 'pointer' && armed) setPointer(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
button.addEventListener('click', () => setPointer(!armed));
|
|
214
|
+
picker.addEventListener('click', (event) => {
|
|
215
|
+
const chip = event.target.closest('.deck-pointer-kind');
|
|
216
|
+
if (chip) setKind(chip.dataset.kind);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// `addKeyBinding` with a descriptor binds the key and lists it in reveal's
|
|
220
|
+
// own help overlay. `C` is unclaimed by reveal.
|
|
221
|
+
deck.addKeyBinding({ keyCode: KEY_CODE, key: 'C', description: 'Toggle the pointer' }, () =>
|
|
222
|
+
setPointer(!armed),
|
|
223
|
+
);
|
|
224
|
+
// While armed: Escape stands it down, the number row picks the kind — the
|
|
225
|
+
// same capture-phase borrowing the pen does with its colours.
|
|
226
|
+
document.addEventListener(
|
|
227
|
+
'keydown',
|
|
228
|
+
(event) => {
|
|
229
|
+
if (!armed || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
230
|
+
if (event.key === 'Escape') setPointer(false);
|
|
231
|
+
else {
|
|
232
|
+
const index = '123'.indexOf(event.key);
|
|
233
|
+
if (index === -1 || index >= KINDS.length) return;
|
|
234
|
+
setKind(KINDS[index].id);
|
|
235
|
+
}
|
|
236
|
+
event.stopPropagation();
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
},
|
|
239
|
+
true,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// No `slidechanged` hook on purpose: the pointer is the cursor, and the
|
|
243
|
+
// cursor does not vanish because the slide moved on. The overview is fair
|
|
244
|
+
// ground for it too.
|
|
245
|
+
window.addEventListener('resize', aim);
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export default RevealPointer;
|
package/runtime/theme.base.css
CHANGED
|
@@ -29,6 +29,39 @@
|
|
|
29
29
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* --- wearing one of reveal's own themes ----------------------------------- */
|
|
33
|
+
|
|
34
|
+
/* `revealTheme:` in slides.config.js loads reveal's theme after this file, where
|
|
35
|
+
it takes the slides. The chrome — the navigator, the badge, the signature —
|
|
36
|
+
is not `.reveal`-scoped, so it would sail past untouched and leave a black
|
|
37
|
+
panel down the side of a white deck.
|
|
38
|
+
|
|
39
|
+
Every reveal 5 theme declares its palette as `--r-*` custom properties, so
|
|
40
|
+
the fix is one indirection rather than fifteen stylesheets: point the tokens
|
|
41
|
+
above at reveal's, and the whole of the chrome follows whatever the deck
|
|
42
|
+
wears. The four reveal has no equivalent for are mixed from the two that
|
|
43
|
+
carry the theme, which keeps the panel's depth on a light theme as well as
|
|
44
|
+
a dark one. */
|
|
45
|
+
html[data-reveal-theme] {
|
|
46
|
+
--bg: var(--r-background-color);
|
|
47
|
+
--text: var(--r-main-color);
|
|
48
|
+
--accent: var(--r-link-color);
|
|
49
|
+
--accent-2: var(--r-link-color-hover, var(--r-link-color));
|
|
50
|
+
--sans: var(--r-main-font);
|
|
51
|
+
--mono: var(--r-code-font);
|
|
52
|
+
--bg-2: color-mix(in srgb, var(--r-background-color) 92%, var(--r-main-color));
|
|
53
|
+
--card: color-mix(in srgb, var(--r-background-color) 86%, var(--r-main-color));
|
|
54
|
+
--line: color-mix(in srgb, var(--r-main-color) 20%, transparent);
|
|
55
|
+
--muted: color-mix(in srgb, var(--r-main-color) 62%, var(--r-background-color));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The base look paints two dark halos on <html>, below the deck. They are the
|
|
59
|
+
wrong atmosphere for a borrowed theme, and on a light one they show through
|
|
60
|
+
wherever the viewport does not reach — so the theme's own flat colour takes
|
|
61
|
+
over. <body> is left alone on purpose: reveal paints the deck's background
|
|
62
|
+
there, and that is the theme's job now. */
|
|
63
|
+
html[data-reveal-theme] { background: var(--bg); }
|
|
64
|
+
|
|
32
65
|
/* reveal.js 5 paints the page background on `.reveal-viewport` (the <html>
|
|
33
66
|
element), so setting it on <body> would sit behind it and never show. */
|
|
34
67
|
html,
|
|
@@ -111,6 +144,7 @@ body,
|
|
|
111
144
|
/* Trailing margin would push centred content off-centre. */
|
|
112
145
|
.reveal .slides section > :last-child { margin-bottom: 0; }
|
|
113
146
|
.reveal strong { color: #fff; font-weight: 650; }
|
|
147
|
+
html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--text)); }
|
|
114
148
|
.reveal em { color: var(--accent-2); font-style: italic; }
|
|
115
149
|
.reveal a { color: var(--accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); }
|
|
116
150
|
|
|
@@ -415,10 +449,11 @@ body,
|
|
|
415
449
|
}
|
|
416
450
|
|
|
417
451
|
/* Faded back until it is wanted: on stage the slide is the point, and the
|
|
418
|
-
toolbar
|
|
452
|
+
toolbar keeps to the quiet strip along the bottom, between the arrows and
|
|
453
|
+
the signature. */
|
|
419
454
|
#deck-tools {
|
|
420
455
|
position: fixed;
|
|
421
|
-
|
|
456
|
+
bottom: 14px;
|
|
422
457
|
left: 50%;
|
|
423
458
|
transform: translateX(-50%);
|
|
424
459
|
z-index: 40;
|
|
@@ -471,10 +506,11 @@ body,
|
|
|
471
506
|
0 0 16px color-mix(in srgb, var(--pen, #ff8c1a) 55%, transparent);
|
|
472
507
|
}
|
|
473
508
|
|
|
474
|
-
/* The colours,
|
|
509
|
+
/* The colours, floating above the toolbar while a tool is armed — the
|
|
510
|
+
toolbar sits on the bottom edge, so up is where the room is. */
|
|
475
511
|
#deck-swatches {
|
|
476
512
|
position: absolute;
|
|
477
|
-
|
|
513
|
+
bottom: calc(100% + 9px);
|
|
478
514
|
left: 50%;
|
|
479
515
|
transform: translateX(-50%);
|
|
480
516
|
display: flex;
|
|
@@ -787,6 +823,142 @@ body.outline-open #deck-outline { transform: none; }
|
|
|
787
823
|
background: color-mix(in srgb, var(--text) 5%, transparent);
|
|
788
824
|
}
|
|
789
825
|
|
|
826
|
+
/* --- the pointer (pointer.js) -------------------------------------------- */
|
|
827
|
+
|
|
828
|
+
/* The real cursor goes away everywhere while the pointer is armed — the
|
|
829
|
+
glowing stand-in below is the cursor now, toolbar included. */
|
|
830
|
+
html.deck-pointer-on,
|
|
831
|
+
html.deck-pointer-on * { cursor: none !important; }
|
|
832
|
+
|
|
833
|
+
/* Above everything, spotlight and ink included: it stands in for the cursor,
|
|
834
|
+
which always rides on top of what it points at. Never touchable. */
|
|
835
|
+
#deck-pointer {
|
|
836
|
+
position: fixed;
|
|
837
|
+
inset: 0;
|
|
838
|
+
z-index: 60;
|
|
839
|
+
pointer-events: none;
|
|
840
|
+
}
|
|
841
|
+
#deck-pointer[hidden] { display: none; }
|
|
842
|
+
|
|
843
|
+
/* Each piece sits at 0,0 and is carried by the `translate` property pointer.js
|
|
844
|
+
writes per frame; `transform` is left free for the press effect, so the two
|
|
845
|
+
compose instead of overwriting each other. */
|
|
846
|
+
/* The halo wears the pen's own orange (annotate.js's palette), not the
|
|
847
|
+
theme accent: on stage the pointer has to burn, not blend in. */
|
|
848
|
+
#deck-pointer { --pointer-hot: #ff8c1a; }
|
|
849
|
+
|
|
850
|
+
.deck-pointer-dot {
|
|
851
|
+
position: absolute;
|
|
852
|
+
left: -3.5px;
|
|
853
|
+
top: -3.5px;
|
|
854
|
+
width: 7px;
|
|
855
|
+
height: 7px;
|
|
856
|
+
border-radius: 999px;
|
|
857
|
+
background: #fff;
|
|
858
|
+
box-shadow:
|
|
859
|
+
0 0 6px 1px rgb(255 255 255 / 0.9),
|
|
860
|
+
0 0 14px 4px color-mix(in srgb, var(--pointer-hot) 70%, transparent);
|
|
861
|
+
transition: transform 0.12s ease;
|
|
862
|
+
}
|
|
863
|
+
/* The ring trails the dot on a short elastic (pointer.js lerps it), lit from
|
|
864
|
+
inside and haloed outside — the trailing is what makes it read as alive. */
|
|
865
|
+
.deck-pointer-halo {
|
|
866
|
+
position: absolute;
|
|
867
|
+
left: -19px;
|
|
868
|
+
top: -19px;
|
|
869
|
+
width: 38px;
|
|
870
|
+
height: 38px;
|
|
871
|
+
border-radius: 999px;
|
|
872
|
+
border: 3px solid color-mix(in srgb, var(--pointer-hot) 90%, #fff);
|
|
873
|
+
background: radial-gradient(circle, color-mix(in srgb, var(--pointer-hot) 30%, transparent) 0%, transparent 68%);
|
|
874
|
+
box-shadow:
|
|
875
|
+
0 0 26px color-mix(in srgb, var(--pointer-hot) 75%, transparent),
|
|
876
|
+
inset 0 0 14px color-mix(in srgb, var(--pointer-hot) 55%, transparent);
|
|
877
|
+
transition: transform 0.16s ease, border-color 0.16s ease;
|
|
878
|
+
}
|
|
879
|
+
/* A click: the dot swells, the ring snaps in around it. */
|
|
880
|
+
#deck-pointer.is-pressed .deck-pointer-dot { transform: scale(1.7); }
|
|
881
|
+
#deck-pointer.is-pressed .deck-pointer-halo {
|
|
882
|
+
transform: scale(0.7);
|
|
883
|
+
border-color: #fff;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/* The compass dart. pointer.js sets `rotate` so it always aims at the centre
|
|
887
|
+
of the viewport; the pivot is its own middle, under the cursor's hotspot. */
|
|
888
|
+
.deck-pointer-compass {
|
|
889
|
+
position: absolute;
|
|
890
|
+
left: -21px;
|
|
891
|
+
top: -21px;
|
|
892
|
+
width: 42px;
|
|
893
|
+
height: 42px;
|
|
894
|
+
fill: var(--pointer-hot);
|
|
895
|
+
filter:
|
|
896
|
+
drop-shadow(0 0 3px rgb(255 255 255 / 0.5))
|
|
897
|
+
drop-shadow(0 0 12px color-mix(in srgb, var(--pointer-hot) 75%, transparent));
|
|
898
|
+
transition: transform 0.12s ease;
|
|
899
|
+
}
|
|
900
|
+
#deck-pointer.is-pressed .deck-pointer-compass { transform: scale(1.22); }
|
|
901
|
+
|
|
902
|
+
/* One kind on screen at a time; the native kind is the system cursor itself,
|
|
903
|
+
so it paints nothing at all. */
|
|
904
|
+
#deck-pointer[data-kind='halo'] .deck-pointer-compass { display: none; }
|
|
905
|
+
#deck-pointer[data-kind='compass'] .deck-pointer-dot,
|
|
906
|
+
#deck-pointer[data-kind='compass'] .deck-pointer-halo { display: none; }
|
|
907
|
+
#deck-pointer[data-kind='native'] > * { display: none; }
|
|
908
|
+
|
|
909
|
+
/* The armed pointer button wears the accent, not the pen's ink — it draws in
|
|
910
|
+
nothing, it glows. The id outweighs the generic armed-tool rule above. */
|
|
911
|
+
#tool-pointer[aria-pressed='true'] {
|
|
912
|
+
background: var(--accent);
|
|
913
|
+
color: #0d1626;
|
|
914
|
+
box-shadow:
|
|
915
|
+
0 0 0 1px color-mix(in srgb, var(--text) 30%, transparent),
|
|
916
|
+
0 0 16px color-mix(in srgb, var(--accent) 55%, transparent);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* The kind picker, floating above the toolbar like the pen's swatches. */
|
|
920
|
+
#deck-pointer-kinds {
|
|
921
|
+
position: absolute;
|
|
922
|
+
bottom: calc(100% + 9px);
|
|
923
|
+
left: 50%;
|
|
924
|
+
transform: translateX(-50%);
|
|
925
|
+
display: flex;
|
|
926
|
+
gap: 6px;
|
|
927
|
+
padding: 5px;
|
|
928
|
+
background: color-mix(in srgb, var(--card) 82%, transparent);
|
|
929
|
+
border: 1px solid var(--line);
|
|
930
|
+
border-radius: 999px;
|
|
931
|
+
backdrop-filter: blur(6px);
|
|
932
|
+
}
|
|
933
|
+
#deck-pointer-kinds[hidden] { display: none; }
|
|
934
|
+
|
|
935
|
+
.deck-pointer-kind {
|
|
936
|
+
display: grid;
|
|
937
|
+
place-items: center;
|
|
938
|
+
width: 26px;
|
|
939
|
+
height: 26px;
|
|
940
|
+
padding: 0;
|
|
941
|
+
border: 0;
|
|
942
|
+
border-radius: 999px;
|
|
943
|
+
background: transparent;
|
|
944
|
+
color: var(--muted);
|
|
945
|
+
cursor: pointer;
|
|
946
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
947
|
+
}
|
|
948
|
+
.deck-pointer-kind svg {
|
|
949
|
+
display: block;
|
|
950
|
+
width: 15px;
|
|
951
|
+
height: 15px;
|
|
952
|
+
}
|
|
953
|
+
.deck-pointer-kind:hover {
|
|
954
|
+
background: color-mix(in srgb, var(--text) 10%, transparent);
|
|
955
|
+
color: var(--text);
|
|
956
|
+
}
|
|
957
|
+
.deck-pointer-kind[aria-checked='true'] {
|
|
958
|
+
background: var(--accent);
|
|
959
|
+
color: #0d1626;
|
|
960
|
+
}
|
|
961
|
+
|
|
790
962
|
/* --- the spotlight (spotlight.js) ---------------------------------------- */
|
|
791
963
|
|
|
792
964
|
/* One veil over the slides, under the ink: spotlight.js punches the lit
|
|
@@ -844,6 +1016,7 @@ body.outline-open #deck-outline { transform: none; }
|
|
|
844
1016
|
#deck-ink,
|
|
845
1017
|
#deck-spot,
|
|
846
1018
|
#deck-spot-ring,
|
|
1019
|
+
#deck-pointer,
|
|
847
1020
|
#deck-tools,
|
|
848
1021
|
#deck-menu,
|
|
849
1022
|
#deck-outline { display: none; }
|
|
@@ -9,6 +9,18 @@ export default {
|
|
|
9
9
|
// Folder behind a bare `<!-- demo: counter -->` marker in the Markdown.
|
|
10
10
|
demos: 'demo',
|
|
11
11
|
|
|
12
|
+
// One of reveal.js's own themes, served from this project rather than from a
|
|
13
|
+
// CDN: beige, black, black-contrast, blood, dracula, league, moon, night,
|
|
14
|
+
// serif, simple, sky, solarized, white, white-contrast. Uncomment to wear one
|
|
15
|
+
// — the deck's chrome follows it. Leave it out for this package's own look.
|
|
16
|
+
// revealTheme: 'dracula',
|
|
17
|
+
|
|
18
|
+
// Six of those themes ask fonts.googleapis.com for their typefaces. Those
|
|
19
|
+
// requests are stripped out, so a deck never depends on the room's wifi; the
|
|
20
|
+
// theme falls back to the next font in its own stack. Set this to true to let
|
|
21
|
+
// them through and get the typography of revealjs.com/themes exactly.
|
|
22
|
+
// webfonts: true,
|
|
23
|
+
|
|
12
24
|
// Port `fb-slides dev` serves the deck on (`preview` uses the next one up).
|
|
13
25
|
// `--port n` on the command line wins over this.
|
|
14
26
|
port: 4000,
|