fb-slides 0.1.2
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/LICENSE +21 -0
- package/README.md +225 -0
- package/bin/fb-slides.mjs +111 -0
- package/lib/build.mjs +98 -0
- package/lib/config.mjs +90 -0
- package/lib/create.mjs +59 -0
- package/lib/decks.mjs +13 -0
- package/lib/dev.mjs +94 -0
- package/lib/render.mjs +52 -0
- package/lib/server.mjs +168 -0
- package/lib/vendor.mjs +32 -0
- package/package.json +51 -0
- package/runtime/annotate.js +458 -0
- package/runtime/deck.js +299 -0
- package/runtime/index.html +37 -0
- package/runtime/outline.js +354 -0
- package/runtime/shortcuts.js +53 -0
- package/runtime/spotlight.js +293 -0
- package/runtime/theme.base.css +879 -0
- package/templates/starter/README.md +27 -0
- package/templates/starter/_gitignore +4 -0
- package/templates/starter/_package.json +14 -0
- package/templates/starter/assets/.gitkeep +0 -0
- package/templates/starter/decks/01-intro.md +44 -0
- package/templates/starter/decks/02-demos.md +34 -0
- package/templates/starter/demo/angular-hello/README.md +15 -0
- package/templates/starter/demo/angular-hello/_package.json +24 -0
- package/templates/starter/demo/angular-hello/angular.json +34 -0
- package/templates/starter/demo/angular-hello/src/index.html +12 -0
- package/templates/starter/demo/angular-hello/src/main.ts +18 -0
- package/templates/starter/demo/angular-hello/src/styles.css +22 -0
- package/templates/starter/demo/angular-hello/tsconfig.app.json +5 -0
- package/templates/starter/demo/angular-hello/tsconfig.json +17 -0
- package/templates/starter/demo/counter/index.html +34 -0
- package/templates/starter/slides.config.js +34 -0
- package/templates/starter/theme.css +14 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// RevealAnnotate — draw on the slide.
|
|
3
|
+
//
|
|
4
|
+
// A reveal.js plugin: `init()` gets the deck instance, so this file owns the
|
|
5
|
+
// whole feature — its chrome, its canvas and its key bindings — and index.html
|
|
6
|
+
// stays a page of slides. Register it in the `plugins:` array like any other.
|
|
7
|
+
//
|
|
8
|
+
// The ink goes on one full-viewport canvas in screen pixels, which keeps it
|
|
9
|
+
// clear of the CSS transform reveal scales a slide with: the pen follows the
|
|
10
|
+
// pointer, not the slide's coordinate space.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import { unlistShortcut } from './shortcuts.js';
|
|
14
|
+
|
|
15
|
+
// The pen's palette. It lives here rather than in theme.css because it is a
|
|
16
|
+
// list the toolbar has to enumerate, not a token the theme swaps: `glow` is the
|
|
17
|
+
// halo, `core` the bright thread down the middle of it. JS hands the selected
|
|
18
|
+
// one back to CSS as `--pen`, which is what colours the armed button.
|
|
19
|
+
const COLOURS = [
|
|
20
|
+
{ label: 'Orange', glow: '#ff8c1a', core: '#ffd6a8' },
|
|
21
|
+
{ label: 'Green', glow: '#35d76f', core: '#c9f7dd' },
|
|
22
|
+
{ label: 'Fire red', glow: '#ff3b20', core: '#ffc7bb' },
|
|
23
|
+
{ label: 'Sky', glow: '#4cc9ff', core: '#d3efff' },
|
|
24
|
+
{ label: 'White', glow: '#ffffff', core: '#ffffff' },
|
|
25
|
+
{ label: 'Black', glow: '#000000', core: '#000000' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// A released stroke rubs itself out from its oldest point towards its newest.
|
|
29
|
+
// FADE_MS is how long that takes end to end, FADE_TAIL how much of the stroke
|
|
30
|
+
// the erase front softens as it passes — at 0 it would be a hard wipe.
|
|
31
|
+
const FADE_MS = 1600;
|
|
32
|
+
const FADE_TAIL = 0.35;
|
|
33
|
+
|
|
34
|
+
// Softness comes from the input, not the curve: resample the pointer onto
|
|
35
|
+
// points that are far enough apart to be signal, and pull each one back towards
|
|
36
|
+
// the last so the hand's own tremor never reaches the canvas.
|
|
37
|
+
const MIN_STEP = 3; // px between recorded points; anything closer is jitter
|
|
38
|
+
const SMOOTHING = 0.45; // how far each raw sample is dragged towards the last
|
|
39
|
+
|
|
40
|
+
// Catmull-Rom through the points, as one cubic per segment. 1/6 is the tension
|
|
41
|
+
// that makes the spline pass through each point with a continuous tangent —
|
|
42
|
+
// higher bulges the curve out past them.
|
|
43
|
+
const TENSION = 1 / 6;
|
|
44
|
+
|
|
45
|
+
const WIDTH = 3.4; // px, the bright core of the line
|
|
46
|
+
|
|
47
|
+
// Two layers, each the same path stroked at a few widths: a soft halo, then the
|
|
48
|
+
// core on top of it. Within a layer the passes stack additively, hence the low
|
|
49
|
+
// alphas. Cheaper than a shadowBlur per segment.
|
|
50
|
+
const LAYERS = [
|
|
51
|
+
{ ink: 'glow', passes: [{ width: 7, alpha: 0.07 }, { width: 3.2, alpha: 0.16 }] },
|
|
52
|
+
{ ink: 'core', passes: [{ width: 1, alpha: 1 }] },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const MAX_WIDTH = WIDTH * Math.max(...LAYERS.flatMap((l) => l.passes.map((p) => p.width)));
|
|
56
|
+
|
|
57
|
+
// Alpha is quantised so consecutive segments sharing a value can go down as one
|
|
58
|
+
// path: a stroke is hundreds of segments, but the fade only varies across the
|
|
59
|
+
// narrow band the erase front is crossing.
|
|
60
|
+
const QUANTA = 24;
|
|
61
|
+
|
|
62
|
+
// `P` is reveal's own previous-slide key, and a plugin binding wins over it —
|
|
63
|
+
// which is the point: on stage the pen is reached for far more often than a key
|
|
64
|
+
// ← 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
|
+
const KEY_CODE = 80;
|
|
67
|
+
const HELP_TAKEN_FROM = { key: 'P', from: /previous/i };
|
|
68
|
+
|
|
69
|
+
const PEN_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
70
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
71
|
+
<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
|
+
</svg>`;
|
|
73
|
+
|
|
74
|
+
// Black text on a light chip, light text on a dark one — the armed pen button
|
|
75
|
+
// wears the ink's own colour, and two of the six are the extremes.
|
|
76
|
+
const readable = (hex) => {
|
|
77
|
+
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
78
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.5 ? '#17110a' : '#f2f5f9';
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// The two control points of the cubic running from `points[i]` to `points[i+1]`,
|
|
82
|
+
// each aimed along the line between that point's own neighbours. Consecutive
|
|
83
|
+
// segments therefore leave and arrive on the same tangent, which is what lets
|
|
84
|
+
// the fade cut the stroke into pieces without the seams showing.
|
|
85
|
+
const controls = (points, i) => {
|
|
86
|
+
const before = points[Math.max(i - 1, 0)];
|
|
87
|
+
const from = points[i];
|
|
88
|
+
const to = points[i + 1];
|
|
89
|
+
const after = points[Math.min(i + 2, points.length - 1)];
|
|
90
|
+
return [
|
|
91
|
+
from.x + (to.x - before.x) * TENSION,
|
|
92
|
+
from.y + (to.y - before.y) * TENSION,
|
|
93
|
+
to.x - (after.x - from.x) * TENSION,
|
|
94
|
+
to.y - (after.y - from.y) * TENSION,
|
|
95
|
+
];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// How opaque a stroke is `u` of the way along itself (0 oldest, 1 newest), when
|
|
99
|
+
// it is `progress` of the way through its fade. `null` is a stroke still being
|
|
100
|
+
// drawn — those do not fade at all.
|
|
101
|
+
const alphaAt = (u, progress) => {
|
|
102
|
+
if (progress === null) return 1;
|
|
103
|
+
const front = progress * (1 + FADE_TAIL) - FADE_TAIL;
|
|
104
|
+
return Math.min(1, Math.max(0, (u - front) / FADE_TAIL));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const quantise = (alpha) => Math.round(alpha * QUANTA) / QUANTA;
|
|
108
|
+
|
|
109
|
+
const RevealAnnotate = () => ({
|
|
110
|
+
id: 'annotate',
|
|
111
|
+
|
|
112
|
+
init(deck) {
|
|
113
|
+
// ---- chrome ---------------------------------------------------------
|
|
114
|
+
// Fixed-position overlays, so they live on <body> next to the deck's other
|
|
115
|
+
// corners rather than inside `.reveal`, whose transforms they must escape.
|
|
116
|
+
const canvas = document.createElement('canvas');
|
|
117
|
+
canvas.id = 'deck-ink';
|
|
118
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
119
|
+
|
|
120
|
+
const toolbar = document.createElement('div');
|
|
121
|
+
toolbar.id = 'deck-tools';
|
|
122
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
123
|
+
toolbar.setAttribute('aria-label', 'Slide tools');
|
|
124
|
+
toolbar.innerHTML = `
|
|
125
|
+
<button id="tool-pen" class="deck-tool" type="button" aria-pressed="false"
|
|
126
|
+
title="Pen — draw on the slide (P)" aria-label="Pen">${PEN_ICON}</button>
|
|
127
|
+
<div id="deck-swatches" role="radiogroup" aria-label="Pen colour" hidden>
|
|
128
|
+
${COLOURS.map(
|
|
129
|
+
(colour, i) => `<button class="deck-swatch" type="button" role="radio"
|
|
130
|
+
aria-checked="${i === 0}" data-colour="${i}" style="--ink:${colour.glow}"
|
|
131
|
+
title="${colour.label}" aria-label="${colour.label}"></button>`,
|
|
132
|
+
).join('')}
|
|
133
|
+
</div>
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
document.body.append(canvas, toolbar);
|
|
137
|
+
const penButton = toolbar.querySelector('#tool-pen');
|
|
138
|
+
const swatches = toolbar.querySelector('#deck-swatches');
|
|
139
|
+
const ctx = canvas.getContext('2d');
|
|
140
|
+
|
|
141
|
+
// ---- the canvas -----------------------------------------------------
|
|
142
|
+
|
|
143
|
+
let width = 0;
|
|
144
|
+
let height = 0;
|
|
145
|
+
let dpr = 1;
|
|
146
|
+
// Built on first use: a deck nobody draws on should not be paying for a
|
|
147
|
+
// second full-screen canvas. See `onLayer` for what it is for.
|
|
148
|
+
let layer = null;
|
|
149
|
+
let layerCtx = null;
|
|
150
|
+
|
|
151
|
+
const resize = () => {
|
|
152
|
+
// Past 2x the extra pixels are free to nobody: this canvas is repainted
|
|
153
|
+
// whole on every frame of a fade, and the ink is a soft glow anyway.
|
|
154
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
155
|
+
width = window.innerWidth;
|
|
156
|
+
height = window.innerHeight;
|
|
157
|
+
// Assigning width/height resets the context, transform included — so
|
|
158
|
+
// scale after, not before, and let the render repaint what was on it.
|
|
159
|
+
canvas.width = Math.round(width * dpr);
|
|
160
|
+
canvas.height = Math.round(height * dpr);
|
|
161
|
+
canvas.style.width = `${width}px`;
|
|
162
|
+
canvas.style.height = `${height}px`;
|
|
163
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
164
|
+
layer = null;
|
|
165
|
+
schedule();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Build a shape on its own layer, then tint it. Every colour has to behave
|
|
169
|
+
// the same, black included, and that rules out drawing an ink-coloured line
|
|
170
|
+
// straight onto the slide: the seams between the runs of a fade only
|
|
171
|
+
// disappear under `lighter`, which is additive and therefore blind to dark
|
|
172
|
+
// ink. So the shape is laid down in white — where `lighter` adds the two
|
|
173
|
+
// halves of a seam pixel back into a whole — and `source-in` then replaces
|
|
174
|
+
// that white with the ink, keeping the coverage it just worked out.
|
|
175
|
+
//
|
|
176
|
+
// Everything is clipped to the stroke's own box: `source-in` clears whatever
|
|
177
|
+
// the source does not cover, and unclipped that is the whole canvas.
|
|
178
|
+
const onLayer = (box, tint, drawShape) => {
|
|
179
|
+
if (!layer) {
|
|
180
|
+
layer = document.createElement('canvas');
|
|
181
|
+
layer.width = canvas.width;
|
|
182
|
+
layer.height = canvas.height;
|
|
183
|
+
layerCtx = layer.getContext('2d');
|
|
184
|
+
}
|
|
185
|
+
layerCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
186
|
+
layerCtx.save();
|
|
187
|
+
layerCtx.beginPath();
|
|
188
|
+
layerCtx.rect(box.x, box.y, box.w, box.h);
|
|
189
|
+
layerCtx.clip();
|
|
190
|
+
layerCtx.clearRect(box.x, box.y, box.w, box.h);
|
|
191
|
+
layerCtx.globalCompositeOperation = 'lighter';
|
|
192
|
+
layerCtx.lineJoin = 'round';
|
|
193
|
+
drawShape(layerCtx);
|
|
194
|
+
layerCtx.globalCompositeOperation = 'source-in';
|
|
195
|
+
layerCtx.fillStyle = tint;
|
|
196
|
+
layerCtx.fillRect(box.x, box.y, box.w, box.h);
|
|
197
|
+
layerCtx.restore();
|
|
198
|
+
|
|
199
|
+
ctx.drawImage(
|
|
200
|
+
layer,
|
|
201
|
+
box.x * dpr, box.y * dpr, box.w * dpr, box.h * dpr,
|
|
202
|
+
box.x, box.y, box.w, box.h,
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ---- strokes --------------------------------------------------------
|
|
207
|
+
// A stroke is its points, the colour it was drawn in, and the moment the
|
|
208
|
+
// pointer left the slide: `releasedAt: null` is one still under the pen.
|
|
209
|
+
|
|
210
|
+
const strokes = [];
|
|
211
|
+
let drawing = null;
|
|
212
|
+
let frame = 0;
|
|
213
|
+
|
|
214
|
+
const addPoint = (event) => {
|
|
215
|
+
const points = drawing.points;
|
|
216
|
+
const previous = points[points.length - 1];
|
|
217
|
+
let { clientX: x, clientY: y } = event;
|
|
218
|
+
|
|
219
|
+
if (previous) {
|
|
220
|
+
x = previous.x + (x - previous.x) * (1 - SMOOTHING);
|
|
221
|
+
y = previous.y + (y - previous.y) * (1 - SMOOTHING);
|
|
222
|
+
if (Math.hypot(x - previous.x, y - previous.y) < MIN_STEP) return;
|
|
223
|
+
}
|
|
224
|
+
points.push({ x, y });
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// The slab of canvas a stroke can reach, clamped to the viewport. Padded by
|
|
228
|
+
// half the widest pass, plus a little for the curve's overshoot between
|
|
229
|
+
// points.
|
|
230
|
+
const boundsOf = (points) => {
|
|
231
|
+
let minX = Infinity;
|
|
232
|
+
let minY = Infinity;
|
|
233
|
+
let maxX = -Infinity;
|
|
234
|
+
let maxY = -Infinity;
|
|
235
|
+
for (const { x, y } of points) {
|
|
236
|
+
if (x < minX) minX = x;
|
|
237
|
+
if (y < minY) minY = y;
|
|
238
|
+
if (x > maxX) maxX = x;
|
|
239
|
+
if (y > maxY) maxY = y;
|
|
240
|
+
}
|
|
241
|
+
const pad = MAX_WIDTH / 2 + 3;
|
|
242
|
+
const x = Math.max(0, Math.floor(minX - pad));
|
|
243
|
+
const y = Math.max(0, Math.floor(minY - pad));
|
|
244
|
+
return {
|
|
245
|
+
x,
|
|
246
|
+
y,
|
|
247
|
+
w: Math.min(width, Math.ceil(maxX + pad)) - x,
|
|
248
|
+
h: Math.min(height, Math.ceil(maxY + pad)) - y,
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Cut the stroke into runs of segments that share a quantised alpha. Runs
|
|
253
|
+
// meet end to end on a shared point and a shared tangent, and are stroked
|
|
254
|
+
// with a butt cap: a round cap would lay a disc over the next run's start,
|
|
255
|
+
// and those are the beads that crawl along a line as it fades.
|
|
256
|
+
const runsOf = (stroke, progress) => {
|
|
257
|
+
const points = stroke.points;
|
|
258
|
+
const segments = points.length - 1;
|
|
259
|
+
const runs = [];
|
|
260
|
+
|
|
261
|
+
let path = null;
|
|
262
|
+
let alpha = -1;
|
|
263
|
+
for (let i = 0; i < segments; i++) {
|
|
264
|
+
// A one-segment stroke has no oldest end to start from: fade it whole.
|
|
265
|
+
const next = quantise(alphaAt(segments === 1 ? 0.5 : i / (segments - 1), progress));
|
|
266
|
+
if (next !== alpha) {
|
|
267
|
+
alpha = next;
|
|
268
|
+
path = null;
|
|
269
|
+
if (alpha > 0) {
|
|
270
|
+
path = new Path2D();
|
|
271
|
+
path.moveTo(points[i].x, points[i].y);
|
|
272
|
+
runs.push({ path, alpha, round: false });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!path) continue;
|
|
276
|
+
path.bezierCurveTo(...controls(points, i), points[i + 1].x, points[i + 1].y);
|
|
277
|
+
}
|
|
278
|
+
// `lineCap` applies to both ends of a path, so a run can only be rounded
|
|
279
|
+
// off when it has no seam to spoil — which is every stroke that is not
|
|
280
|
+
// mid-fade, the pen's own line included.
|
|
281
|
+
if (runs.length === 1) runs[0].round = true;
|
|
282
|
+
return runs;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Returns false once the stroke has faded out and can be dropped.
|
|
286
|
+
const paint = (stroke, now) => {
|
|
287
|
+
const progress = stroke.releasedAt === null ? null : (now - stroke.releasedAt) / FADE_MS;
|
|
288
|
+
if (progress !== null && progress >= 1) return false;
|
|
289
|
+
|
|
290
|
+
const box = boundsOf(stroke.points);
|
|
291
|
+
if (box.w <= 0 || box.h <= 0) return true;
|
|
292
|
+
|
|
293
|
+
// Pressed without moving: the pen still left a mark.
|
|
294
|
+
if (stroke.points.length < 2) {
|
|
295
|
+
const alpha = progress === null ? 1 : 1 - progress;
|
|
296
|
+
const [point] = stroke.points;
|
|
297
|
+
for (const { ink, passes } of LAYERS) {
|
|
298
|
+
onLayer(box, stroke.ink[ink], (c) => {
|
|
299
|
+
for (const pass of passes) {
|
|
300
|
+
c.beginPath();
|
|
301
|
+
c.arc(point.x, point.y, (WIDTH * pass.width) / 2, 0, Math.PI * 2);
|
|
302
|
+
c.fillStyle = `rgba(255, 255, 255, ${alpha * pass.alpha})`;
|
|
303
|
+
c.fill();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const runs = runsOf(stroke, progress);
|
|
311
|
+
for (const { ink, passes } of LAYERS) {
|
|
312
|
+
onLayer(box, stroke.ink[ink], (c) => {
|
|
313
|
+
for (const pass of passes) {
|
|
314
|
+
c.lineWidth = WIDTH * pass.width;
|
|
315
|
+
for (const run of runs) {
|
|
316
|
+
c.lineCap = run.round ? 'round' : 'butt';
|
|
317
|
+
c.strokeStyle = `rgba(255, 255, 255, ${run.alpha * pass.alpha})`;
|
|
318
|
+
c.stroke(run.path);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const render = () => {
|
|
327
|
+
frame = 0;
|
|
328
|
+
const now = performance.now();
|
|
329
|
+
|
|
330
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
331
|
+
ctx.clearRect(0, 0, width, height);
|
|
332
|
+
|
|
333
|
+
for (let i = strokes.length - 1; i >= 0; i--) {
|
|
334
|
+
if (!paint(strokes[i], now)) strokes.splice(i, 1);
|
|
335
|
+
}
|
|
336
|
+
// Only a fading stroke needs the next frame. A live one changes when a
|
|
337
|
+
// point is added and not before, so a pen held still costs nothing —
|
|
338
|
+
// without this the loop repaints a full-screen canvas forever.
|
|
339
|
+
if (strokes.some((stroke) => stroke.releasedAt !== null)) schedule();
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const schedule = () => {
|
|
343
|
+
frame ||= requestAnimationFrame(render);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const clear = () => {
|
|
347
|
+
drawing = null;
|
|
348
|
+
strokes.length = 0;
|
|
349
|
+
ctx.clearRect(0, 0, width, height);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// ---- the pen --------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
let penOn = false;
|
|
355
|
+
let ink = COLOURS[0];
|
|
356
|
+
|
|
357
|
+
const setInk = (colour) => {
|
|
358
|
+
ink = colour;
|
|
359
|
+
// Handed back to CSS: the armed button wears the colour it draws in.
|
|
360
|
+
toolbar.style.setProperty('--pen', colour.glow);
|
|
361
|
+
toolbar.style.setProperty('--pen-ink', readable(colour.glow));
|
|
362
|
+
for (const button of swatches.children) {
|
|
363
|
+
button.setAttribute('aria-checked', String(COLOURS[button.dataset.colour] === colour));
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const setPen = (on) => {
|
|
368
|
+
penOn = on;
|
|
369
|
+
penButton.setAttribute('aria-pressed', String(on));
|
|
370
|
+
toolbar.classList.toggle('is-armed', on);
|
|
371
|
+
// The colours are only worth showing while there is a pen to apply them
|
|
372
|
+
// to, which makes arming it the one gesture that opens them.
|
|
373
|
+
swatches.hidden = !on;
|
|
374
|
+
// Only an armed canvas takes the pointer; the rest of the time clicks
|
|
375
|
+
// fall through to the slide underneath.
|
|
376
|
+
canvas.classList.toggle('is-live', on);
|
|
377
|
+
if (on) document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'pen' }));
|
|
378
|
+
else clear();
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Two tools, one pointer: arming either disarms the other. The handshake
|
|
382
|
+
// is a DOM event rather than an import either way round, so each plugin
|
|
383
|
+
// works alone.
|
|
384
|
+
document.addEventListener('deck:tool-armed', (event) => {
|
|
385
|
+
if (event.detail !== 'pen' && penOn) setPen(false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
canvas.addEventListener('pointerdown', (event) => {
|
|
389
|
+
if (!penOn || event.button !== 0) return;
|
|
390
|
+
canvas.setPointerCapture(event.pointerId);
|
|
391
|
+
// The colour is caught at the down-stroke, so a line already fading keeps
|
|
392
|
+
// the one it was drawn in when you pick another.
|
|
393
|
+
drawing = { id: event.pointerId, points: [], releasedAt: null, ink };
|
|
394
|
+
strokes.push(drawing);
|
|
395
|
+
addPoint(event);
|
|
396
|
+
schedule();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
canvas.addEventListener('pointermove', (event) => {
|
|
400
|
+
if (!drawing || event.pointerId !== drawing.id) return;
|
|
401
|
+
// A trackpad reports faster than the display refreshes, and the samples
|
|
402
|
+
// in between are what keeps a quick curve smooth instead of faceted.
|
|
403
|
+
// The list comes back empty where there is nothing to coalesce, which is
|
|
404
|
+
// still a move worth recording — hence the event itself as the fallback.
|
|
405
|
+
const samples = event.getCoalescedEvents?.() ?? [];
|
|
406
|
+
for (const sample of samples.length ? samples : [event]) addPoint(sample);
|
|
407
|
+
schedule();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const release = (event) => {
|
|
411
|
+
if (!drawing || event.pointerId !== drawing.id) return;
|
|
412
|
+
drawing.releasedAt = performance.now();
|
|
413
|
+
drawing = null;
|
|
414
|
+
schedule();
|
|
415
|
+
};
|
|
416
|
+
canvas.addEventListener('pointerup', release);
|
|
417
|
+
canvas.addEventListener('pointercancel', release);
|
|
418
|
+
|
|
419
|
+
penButton.addEventListener('click', () => setPen(!penOn));
|
|
420
|
+
swatches.addEventListener('click', (event) => {
|
|
421
|
+
const button = event.target.closest('.deck-swatch');
|
|
422
|
+
if (button) setInk(COLOURS[button.dataset.colour]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// `addKeyBinding` with a descriptor binds the key and lists it in reveal's
|
|
426
|
+
// own help overlay, which `registerKeyboardShortcut` alone would not do.
|
|
427
|
+
deck.addKeyBinding({ keyCode: KEY_CODE, key: 'P', description: 'Toggle the pen' }, () =>
|
|
428
|
+
setPen(!penOn),
|
|
429
|
+
);
|
|
430
|
+
// Escape is reveal's own overview toggle, and it binds before a plugin can.
|
|
431
|
+
// Capture phase gets there first — but only to take Escape back while the
|
|
432
|
+
// pen is armed, so the key still opens the overview the rest of the time.
|
|
433
|
+
document.addEventListener(
|
|
434
|
+
'keydown',
|
|
435
|
+
(event) => {
|
|
436
|
+
if (event.key !== 'Escape' || !penOn) return;
|
|
437
|
+
setPen(false);
|
|
438
|
+
event.stopPropagation();
|
|
439
|
+
event.preventDefault();
|
|
440
|
+
},
|
|
441
|
+
true,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Only once reveal has configured itself is there a row to correct.
|
|
445
|
+
deck.on('ready', () => unlistShortcut(deck, HELP_TAKEN_FROM));
|
|
446
|
+
|
|
447
|
+
// A new slide is a clean sheet — including a line still under the pointer.
|
|
448
|
+
deck.on('slidechanged', clear);
|
|
449
|
+
// The overview is a different surface; drawing over it would annotate nothing.
|
|
450
|
+
deck.on('overviewshown', () => setPen(false));
|
|
451
|
+
|
|
452
|
+
window.addEventListener('resize', resize);
|
|
453
|
+
setInk(ink);
|
|
454
|
+
resize();
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
export default RevealAnnotate;
|