@abreen/tada 1.15.0 → 1.15.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/init/content/labs/02/index.md +22 -7
- package/package.json +1 -1
- package/src/question/style.scss +13 -5
- package/src/slides/index.ts +504 -8
- package/src/slides/style.scss +55 -7
- package/templates/_theme.scss +2 -2
|
@@ -28,10 +28,8 @@ Selection sort's strength is its simplicity.
|
|
|
28
28
|
- `index_smallest()` always scans from `start` to the end
|
|
29
29
|
- It requires no additional memory (other than local variables)
|
|
30
30
|
|
|
31
|
-
However
|
|
32
|
-
|
|
33
|
-
- It has no `break` statements & can't return early
|
|
34
|
-
- No difference between best, average and worst cases
|
|
31
|
+
However, it has no `break` statements and therefore can't return early if
|
|
32
|
+
the array is already sorted.
|
|
35
33
|
|
|
36
34
|
---
|
|
37
35
|
|
|
@@ -48,10 +46,10 @@ However
|
|
|
48
46
|
The innermost statement compares `nums[i]` and `nums[curr]`.
|
|
49
47
|
|
|
50
48
|
- when `i` is 0, the comparison is executed $n - 1$ times
|
|
51
|
-
- when `i` is 1, the
|
|
49
|
+
- when `i` is 1, the comparison is executed $n - 2$ times
|
|
52
50
|
- $\dots$
|
|
53
|
-
- when `i` is $n - 3$, the
|
|
54
|
-
- when `i` is $n - 2$, the
|
|
51
|
+
- when `i` is $n - 3$, the comparison is executed two times
|
|
52
|
+
- when `i` is $n - 2$, the comparison is executed one time
|
|
55
53
|
|
|
56
54
|
Therefore the total number of comparisons is
|
|
57
55
|
|
|
@@ -59,3 +57,20 @@ $$
|
|
|
59
57
|
(n-1) + (n-2) + \dots + 1 = \sum_{i=1}^{n-1}i
|
|
60
58
|
$$
|
|
61
59
|
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Review
|
|
63
|
+
|
|
64
|
+
??? question The outer loop of selection sort runs
|
|
65
|
+
- [ ] $n$ times
|
|
66
|
+
- [x] $n - 1$ times
|
|
67
|
+
- [ ] $n^2$ times
|
|
68
|
+
- [ ] $n - 2$ times
|
|
69
|
+
???
|
|
70
|
+
|
|
71
|
+
??? question What is selection sort's best case?
|
|
72
|
+
- [ ] An already sorted array
|
|
73
|
+
- [ ] A reverse-sorted array
|
|
74
|
+
- [ ] An array filled with the same element
|
|
75
|
+
- [x] Its best, worst, and average cases are all the same
|
|
76
|
+
???
|
package/package.json
CHANGED
package/src/question/style.scss
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
.question {
|
|
4
4
|
--question-label-width: 1.2em;
|
|
5
5
|
|
|
6
|
-
margin-bottom:
|
|
6
|
+
margin-bottom: 1rem;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.question-q,
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
.js .question-a-body[data-revealed] {
|
|
116
116
|
cursor: unset;
|
|
117
117
|
|
|
118
|
-
@include user-select(
|
|
118
|
+
@include user-select(unset);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
.question-multiple-choice {
|
|
@@ -136,13 +136,13 @@
|
|
|
136
136
|
.question-multiple-choice-options {
|
|
137
137
|
display: flex;
|
|
138
138
|
flex-direction: column;
|
|
139
|
-
gap: 0.
|
|
139
|
+
gap: 0.5em;
|
|
140
140
|
margin-left: calc(var(--question-label-width) + 0.5em);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
.question-multiple-choice-option {
|
|
144
144
|
position: relative;
|
|
145
|
-
padding: 0.
|
|
145
|
+
padding: 0.25em calc(var(--question-result-icon-size) + 1em) 0.25em 0.5em;
|
|
146
146
|
cursor: pointer;
|
|
147
147
|
border: 0.125em solid var(--bg2-color);
|
|
148
148
|
border-radius: var(--border-radius);
|
|
@@ -156,6 +156,14 @@
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
.question-multiple-choice-option:has(> code):not(
|
|
160
|
+
:has(> :not(code, .question-multiple-choice-result))
|
|
161
|
+
)
|
|
162
|
+
> code {
|
|
163
|
+
padding: 0;
|
|
164
|
+
background: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
159
167
|
.question-multiple-choice-option::after {
|
|
160
168
|
position: absolute;
|
|
161
169
|
top: 50%;
|
|
@@ -177,7 +185,7 @@
|
|
|
177
185
|
.question-multiple-choice-option {
|
|
178
186
|
cursor: unset;
|
|
179
187
|
|
|
180
|
-
@include user-select(
|
|
188
|
+
@include user-select(unset);
|
|
181
189
|
|
|
182
190
|
&:focus,
|
|
183
191
|
&:focus-visible {
|
package/src/slides/index.ts
CHANGED
|
@@ -9,7 +9,37 @@ interface SlidesPresentDetail {
|
|
|
9
9
|
slideIndex?: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface AnnotationPoint {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AnnotationStroke {
|
|
18
|
+
type: 'stroke';
|
|
19
|
+
points: AnnotationPoint[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AnnotationErase {
|
|
23
|
+
type: 'erase';
|
|
24
|
+
points: AnnotationPoint[];
|
|
25
|
+
radius: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type AnnotationOperation = AnnotationStroke | AnnotationErase;
|
|
29
|
+
|
|
30
|
+
interface SlideAnnotationState {
|
|
31
|
+
canvas: HTMLCanvasElement;
|
|
32
|
+
operations: AnnotationOperation[];
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
dpr: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
const FULLSCREEN_STORAGE_KEY = 'slidesFullscreen';
|
|
39
|
+
const ANNOTATION_CANVAS_SELECTOR = '[data-slides-annotations]';
|
|
40
|
+
const ANNOTATION_COLOR = 'blueviolet';
|
|
41
|
+
const ANNOTATION_ERASER_RADIUS = 18;
|
|
42
|
+
const ANNOTATION_ERASER_DIAMETER = ANNOTATION_ERASER_RADIUS * 2;
|
|
13
43
|
|
|
14
44
|
function isInteractive(
|
|
15
45
|
view: Window & typeof globalThis,
|
|
@@ -111,15 +141,39 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
111
141
|
overlay.append(closeButton);
|
|
112
142
|
document.body.appendChild(overlay);
|
|
113
143
|
|
|
144
|
+
const eraserPreview = document.createElement('div');
|
|
145
|
+
eraserPreview.hidden = true;
|
|
146
|
+
eraserPreview.dataset.slidesEraserPreview = '';
|
|
147
|
+
eraserPreview.setAttribute('aria-hidden', 'true');
|
|
148
|
+
eraserPreview.style.setProperty(
|
|
149
|
+
'--slides-eraser-preview-size',
|
|
150
|
+
`${ANNOTATION_ERASER_DIAMETER}px`,
|
|
151
|
+
);
|
|
152
|
+
document.body.appendChild(eraserPreview);
|
|
153
|
+
|
|
114
154
|
let isPresenting = false;
|
|
115
155
|
let activeIndex = -1;
|
|
116
156
|
let presentationMode: PresentationMode | null = null;
|
|
117
157
|
let isToolbarPinnedVisible = false;
|
|
158
|
+
let isAnnotating = false;
|
|
159
|
+
let isShiftPressed = false;
|
|
160
|
+
let isErasing = false;
|
|
161
|
+
let activeAnnotation: {
|
|
162
|
+
slide: HTMLElement;
|
|
163
|
+
stroke: AnnotationStroke;
|
|
164
|
+
} | null = null;
|
|
165
|
+
let activeEraser: { slide: HTMLElement; erase: AnnotationErase } | null =
|
|
166
|
+
null;
|
|
118
167
|
|
|
119
168
|
const toolbarStates = new WeakMap<HTMLElement, TraceToolbarState>();
|
|
120
169
|
const slideTabIndexes = new WeakMap<HTMLElement, string | null>();
|
|
170
|
+
const annotationStates = new WeakMap<HTMLElement, SlideAnnotationState>();
|
|
121
171
|
|
|
122
172
|
function hidePresentationCursor(): void {
|
|
173
|
+
if (isAnnotating) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
123
177
|
document.body.classList.add('is-presentation-cursor-hidden');
|
|
124
178
|
}
|
|
125
179
|
|
|
@@ -194,8 +248,12 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
194
248
|
}
|
|
195
249
|
|
|
196
250
|
function setActiveSlide(index: number): void {
|
|
251
|
+
activeAnnotation = null;
|
|
252
|
+
activeEraser = null;
|
|
253
|
+
hideEraserPreview();
|
|
197
254
|
activeIndex = Math.max(0, Math.min(index, slides.length - 1));
|
|
198
255
|
updateActiveSlide(slides, activeIndex, isPresenting);
|
|
256
|
+
resizeActiveAnnotationCanvas();
|
|
199
257
|
}
|
|
200
258
|
|
|
201
259
|
function focusSlide(slide: HTMLElement | undefined): void {
|
|
@@ -224,11 +282,21 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
224
282
|
}
|
|
225
283
|
}
|
|
226
284
|
|
|
285
|
+
function scrollToSlide(index: number): void {
|
|
286
|
+
const slide = slides[index];
|
|
287
|
+
if (!slide || typeof slide.scrollIntoView !== 'function') {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
slide.scrollIntoView({ block: 'start', inline: 'nearest' });
|
|
292
|
+
}
|
|
293
|
+
|
|
227
294
|
function exitPresentation(): void {
|
|
228
295
|
if (!isPresenting) {
|
|
229
296
|
return;
|
|
230
297
|
}
|
|
231
298
|
|
|
299
|
+
const exitingActiveIndex = activeIndex;
|
|
232
300
|
const shouldExitFullscreen =
|
|
233
301
|
presentationMode === 'fullscreen' &&
|
|
234
302
|
domDocument.fullscreenElement != null &&
|
|
@@ -237,6 +305,9 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
237
305
|
isPresenting = false;
|
|
238
306
|
activeIndex = -1;
|
|
239
307
|
presentationMode = null;
|
|
308
|
+
isShiftPressed = false;
|
|
309
|
+
setAnnotationMode(false);
|
|
310
|
+
clearAnnotationCanvases();
|
|
240
311
|
hidePresentationControls();
|
|
241
312
|
showPresentationCursor();
|
|
242
313
|
document.body.classList.remove('is-presenting');
|
|
@@ -245,8 +316,16 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
245
316
|
setTraceToolbarVisibility(false);
|
|
246
317
|
|
|
247
318
|
if (shouldExitFullscreen) {
|
|
248
|
-
void domDocument
|
|
319
|
+
void domDocument
|
|
320
|
+
.exitFullscreen()
|
|
321
|
+
.finally(() => scrollToSlide(exitingActiveIndex))
|
|
322
|
+
.catch(() => {
|
|
323
|
+
// ignored
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
249
326
|
}
|
|
327
|
+
|
|
328
|
+
scrollToSlide(exitingActiveIndex);
|
|
250
329
|
}
|
|
251
330
|
|
|
252
331
|
function resetReadyTraces(scope: HTMLElement = slidesRoot): void {
|
|
@@ -271,6 +350,7 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
271
350
|
function enterPresentation(mode: PresentationMode, slideIndex = 0): void {
|
|
272
351
|
isPresenting = true;
|
|
273
352
|
presentationMode = mode;
|
|
353
|
+
setAnnotationMode(false);
|
|
274
354
|
hidePresentationControls();
|
|
275
355
|
showPresentationCursor();
|
|
276
356
|
document.body.classList.add('is-presenting');
|
|
@@ -410,6 +490,12 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
410
490
|
return;
|
|
411
491
|
}
|
|
412
492
|
|
|
493
|
+
if (event.key === 'Shift') {
|
|
494
|
+
isShiftPressed = true;
|
|
495
|
+
setEraserMode(isAnnotating);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
413
499
|
if (
|
|
414
500
|
(event.key === 'ArrowRight' ||
|
|
415
501
|
event.key === 'ArrowLeft' ||
|
|
@@ -440,19 +526,410 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
440
526
|
return;
|
|
441
527
|
}
|
|
442
528
|
|
|
443
|
-
|
|
444
|
-
if (!activeSlide) {
|
|
529
|
+
if (!(event.target instanceof domView.Node)) {
|
|
445
530
|
return;
|
|
446
531
|
}
|
|
447
532
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
533
|
+
move(1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function getActiveSlide(): HTMLElement | null {
|
|
537
|
+
return activeIndex >= 0 ? (slides[activeIndex] ?? null) : null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function setAnnotationMode(enabled: boolean): void {
|
|
541
|
+
isAnnotating = enabled;
|
|
542
|
+
activeAnnotation = null;
|
|
543
|
+
activeEraser = null;
|
|
544
|
+
hideEraserPreview();
|
|
545
|
+
document.body.classList.toggle('is-slides-annotating', enabled);
|
|
546
|
+
setEraserMode(enabled && isShiftPressed);
|
|
547
|
+
|
|
548
|
+
if (enabled) {
|
|
549
|
+
showPresentationCursor();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function setEraserMode(enabled: boolean): void {
|
|
554
|
+
isErasing = enabled && isPresenting && isAnnotating;
|
|
555
|
+
activeEraser = null;
|
|
556
|
+
|
|
557
|
+
if (isErasing) {
|
|
558
|
+
activeAnnotation = null;
|
|
559
|
+
} else {
|
|
560
|
+
hideEraserPreview();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
document.body.classList.toggle('is-slides-erasing', isErasing);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function hideEraserPreview(): void {
|
|
567
|
+
eraserPreview.hidden = true;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function showEraserPreview(event: PointerEvent): void {
|
|
571
|
+
eraserPreview.hidden = false;
|
|
572
|
+
eraserPreview.style.left = `${event.clientX}px`;
|
|
573
|
+
eraserPreview.style.top = `${event.clientY}px`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function toggleAnnotationMode(): void {
|
|
577
|
+
setAnnotationMode(!isAnnotating);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function getSlidePoint(
|
|
581
|
+
slide: HTMLElement,
|
|
582
|
+
event: Pick<PointerEvent, 'clientX' | 'clientY'>,
|
|
583
|
+
): AnnotationPoint | null {
|
|
584
|
+
const rect = slide.getBoundingClientRect();
|
|
585
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const x = Math.min(Math.max(event.clientX - rect.left, 0), rect.width);
|
|
590
|
+
const y = Math.min(Math.max(event.clientY - rect.top, 0), rect.height);
|
|
591
|
+
|
|
592
|
+
return { x: x / rect.width, y: y / rect.height };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function eventIsInsideSlide(
|
|
596
|
+
slide: HTMLElement,
|
|
597
|
+
event: Pick<PointerEvent, 'clientX' | 'clientY'>,
|
|
598
|
+
): boolean {
|
|
599
|
+
const rect = slide.getBoundingClientRect();
|
|
600
|
+
|
|
601
|
+
return (
|
|
602
|
+
event.clientX >= rect.left &&
|
|
603
|
+
event.clientX <= rect.right &&
|
|
604
|
+
event.clientY >= rect.top &&
|
|
605
|
+
event.clientY <= rect.bottom
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function getCanvasContext(
|
|
610
|
+
canvas: HTMLCanvasElement,
|
|
611
|
+
): CanvasRenderingContext2D | null {
|
|
612
|
+
try {
|
|
613
|
+
return canvas.getContext('2d');
|
|
614
|
+
} catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function getAnnotationState(slide: HTMLElement): SlideAnnotationState {
|
|
620
|
+
const existing = annotationStates.get(slide);
|
|
621
|
+
if (existing) {
|
|
622
|
+
return existing;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const canvas = document.createElement('canvas');
|
|
626
|
+
canvas.dataset.slidesAnnotations = '';
|
|
627
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
628
|
+
slide.append(canvas);
|
|
629
|
+
|
|
630
|
+
const state: SlideAnnotationState = {
|
|
631
|
+
canvas,
|
|
632
|
+
operations: [],
|
|
633
|
+
width: 0,
|
|
634
|
+
height: 0,
|
|
635
|
+
dpr: 1,
|
|
636
|
+
};
|
|
637
|
+
annotationStates.set(slide, state);
|
|
638
|
+
resizeAnnotationCanvas(slide, state);
|
|
639
|
+
|
|
640
|
+
return state;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function resizeAnnotationCanvas(
|
|
644
|
+
slide: HTMLElement,
|
|
645
|
+
state: SlideAnnotationState,
|
|
646
|
+
): void {
|
|
647
|
+
const rect = slide.getBoundingClientRect();
|
|
648
|
+
const width = Math.max(1, rect.width);
|
|
649
|
+
const height = Math.max(1, rect.height);
|
|
650
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
651
|
+
|
|
652
|
+
if (state.width === width && state.height === height && state.dpr === dpr) {
|
|
452
653
|
return;
|
|
453
654
|
}
|
|
454
655
|
|
|
455
|
-
|
|
656
|
+
state.width = width;
|
|
657
|
+
state.height = height;
|
|
658
|
+
state.dpr = dpr;
|
|
659
|
+
state.canvas.width = Math.round(width * dpr);
|
|
660
|
+
state.canvas.height = Math.round(height * dpr);
|
|
661
|
+
redrawAnnotationCanvas(state);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function resizeActiveAnnotationCanvas(): void {
|
|
665
|
+
const slide = getActiveSlide();
|
|
666
|
+
if (!slide) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const state = annotationStates.get(slide);
|
|
671
|
+
if (state) {
|
|
672
|
+
resizeAnnotationCanvas(slide, state);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function redrawAnnotationCanvas(state: SlideAnnotationState): void {
|
|
677
|
+
const ctx = getCanvasContext(state.canvas);
|
|
678
|
+
if (!ctx) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
|
|
683
|
+
|
|
684
|
+
for (const operation of state.operations) {
|
|
685
|
+
if (operation.type === 'stroke') {
|
|
686
|
+
drawAnnotationStroke(ctx, operation, state);
|
|
687
|
+
} else {
|
|
688
|
+
drawAnnotationErase(ctx, operation, state);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function drawAnnotationStroke(
|
|
694
|
+
ctx: CanvasRenderingContext2D,
|
|
695
|
+
stroke: AnnotationStroke,
|
|
696
|
+
state: SlideAnnotationState,
|
|
697
|
+
): void {
|
|
698
|
+
if (stroke.points.length === 0) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
703
|
+
ctx.strokeStyle = ANNOTATION_COLOR;
|
|
704
|
+
ctx.lineWidth = 3 * state.dpr;
|
|
705
|
+
ctx.lineCap = 'round';
|
|
706
|
+
ctx.lineJoin = 'round';
|
|
707
|
+
ctx.beginPath();
|
|
708
|
+
const [first] = stroke.points;
|
|
709
|
+
ctx.moveTo(first.x * state.canvas.width, first.y * state.canvas.height);
|
|
710
|
+
|
|
711
|
+
for (const point of stroke.points.slice(1)) {
|
|
712
|
+
ctx.lineTo(point.x * state.canvas.width, point.y * state.canvas.height);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
ctx.stroke();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function drawAnnotationErase(
|
|
719
|
+
ctx: CanvasRenderingContext2D,
|
|
720
|
+
erase: AnnotationErase,
|
|
721
|
+
state: SlideAnnotationState,
|
|
722
|
+
): void {
|
|
723
|
+
if (erase.points.length === 0) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const radius = erase.radius * state.dpr;
|
|
728
|
+
ctx.globalCompositeOperation = 'destination-out';
|
|
729
|
+
ctx.lineWidth = radius * 2;
|
|
730
|
+
ctx.lineCap = 'round';
|
|
731
|
+
ctx.lineJoin = 'round';
|
|
732
|
+
|
|
733
|
+
const [first] = erase.points;
|
|
734
|
+
if (erase.points.length === 1) {
|
|
735
|
+
ctx.beginPath();
|
|
736
|
+
ctx.arc(
|
|
737
|
+
first.x * state.canvas.width,
|
|
738
|
+
first.y * state.canvas.height,
|
|
739
|
+
radius,
|
|
740
|
+
0,
|
|
741
|
+
Math.PI * 2,
|
|
742
|
+
);
|
|
743
|
+
ctx.fill();
|
|
744
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
ctx.beginPath();
|
|
749
|
+
ctx.moveTo(first.x * state.canvas.width, first.y * state.canvas.height);
|
|
750
|
+
|
|
751
|
+
for (const point of erase.points.slice(1)) {
|
|
752
|
+
ctx.lineTo(point.x * state.canvas.width, point.y * state.canvas.height);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
ctx.stroke();
|
|
756
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function drawCurrentAnnotation(): void {
|
|
760
|
+
if (!activeAnnotation) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const state = getAnnotationState(activeAnnotation.slide);
|
|
765
|
+
resizeAnnotationCanvas(activeAnnotation.slide, state);
|
|
766
|
+
redrawAnnotationCanvas(state);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function drawCurrentEraser(): void {
|
|
770
|
+
if (!activeEraser) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const state = annotationStates.get(activeEraser.slide);
|
|
775
|
+
if (!state) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
resizeAnnotationCanvas(activeEraser.slide, state);
|
|
780
|
+
redrawAnnotationCanvas(state);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function clearAnnotationCanvases(): void {
|
|
784
|
+
for (const slide of slides) {
|
|
785
|
+
slide.querySelector(ANNOTATION_CANVAS_SELECTOR)?.remove();
|
|
786
|
+
annotationStates.delete(slide);
|
|
787
|
+
}
|
|
788
|
+
activeAnnotation = null;
|
|
789
|
+
activeEraser = null;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function handleAnnotationContextMenu(event: MouseEvent): void {
|
|
793
|
+
if (!isPresenting) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
event.preventDefault();
|
|
798
|
+
event.stopPropagation();
|
|
799
|
+
toggleAnnotationMode();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function handleAnnotationPointerDown(event: PointerEvent): void {
|
|
803
|
+
if (!isPresenting || !isAnnotating || event.button !== 0) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
event.preventDefault();
|
|
808
|
+
event.stopPropagation();
|
|
809
|
+
|
|
810
|
+
if (isErasing) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const slide = getActiveSlide();
|
|
815
|
+
if (!slide || !eventIsInsideSlide(slide, event)) {
|
|
816
|
+
activeAnnotation = null;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const point = getSlidePoint(slide, event);
|
|
821
|
+
if (!point) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const state = getAnnotationState(slide);
|
|
826
|
+
const stroke: AnnotationStroke = { type: 'stroke', points: [point] };
|
|
827
|
+
state.operations.push(stroke);
|
|
828
|
+
activeAnnotation = { slide, stroke };
|
|
829
|
+
drawCurrentAnnotation();
|
|
830
|
+
|
|
831
|
+
const target =
|
|
832
|
+
event.target instanceof domView.Element ? event.target : slide;
|
|
833
|
+
target.setPointerCapture?.(event.pointerId);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function handleAnnotationPointerMove(event: PointerEvent): void {
|
|
837
|
+
if (!isPresenting || !isAnnotating) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (isErasing) {
|
|
842
|
+
handleAnnotationEraseMove(event);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (!activeAnnotation) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
event.preventDefault();
|
|
851
|
+
event.stopPropagation();
|
|
852
|
+
|
|
853
|
+
const point = getSlidePoint(activeAnnotation.slide, event);
|
|
854
|
+
if (!point) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
activeAnnotation.stroke.points.push(point);
|
|
859
|
+
drawCurrentAnnotation();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function handleAnnotationEraseMove(event: PointerEvent): void {
|
|
863
|
+
const slide = getActiveSlide();
|
|
864
|
+
if (!slide || !eventIsInsideSlide(slide, event)) {
|
|
865
|
+
activeEraser = null;
|
|
866
|
+
hideEraserPreview();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const state = annotationStates.get(slide);
|
|
871
|
+
if (!state) {
|
|
872
|
+
activeEraser = null;
|
|
873
|
+
hideEraserPreview();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const point = getSlidePoint(slide, event);
|
|
878
|
+
if (!point) {
|
|
879
|
+
activeEraser = null;
|
|
880
|
+
hideEraserPreview();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
event.preventDefault();
|
|
885
|
+
event.stopPropagation();
|
|
886
|
+
showEraserPreview(event);
|
|
887
|
+
|
|
888
|
+
if (!activeEraser || activeEraser.slide !== slide) {
|
|
889
|
+
const erase: AnnotationErase = {
|
|
890
|
+
type: 'erase',
|
|
891
|
+
points: [],
|
|
892
|
+
radius: ANNOTATION_ERASER_RADIUS,
|
|
893
|
+
};
|
|
894
|
+
state.operations.push(erase);
|
|
895
|
+
activeEraser = { slide, erase };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
activeEraser.erase.points.push(point);
|
|
899
|
+
drawCurrentEraser();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function handleAnnotationPointerUp(event: PointerEvent): void {
|
|
903
|
+
if (!isPresenting || !isAnnotating || !activeAnnotation) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
event.preventDefault();
|
|
908
|
+
event.stopPropagation();
|
|
909
|
+
activeAnnotation = null;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function handleKeyup(event: KeyboardEvent): void {
|
|
913
|
+
if (event.key !== 'Shift') {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
isShiftPressed = false;
|
|
918
|
+
setEraserMode(false);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function handleWindowBlur(): void {
|
|
922
|
+
isShiftPressed = false;
|
|
923
|
+
setEraserMode(false);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function handleAnnotationClick(event: MouseEvent): void {
|
|
927
|
+
if (!isPresenting || !isAnnotating) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
event.preventDefault();
|
|
932
|
+
event.stopPropagation();
|
|
456
933
|
}
|
|
457
934
|
|
|
458
935
|
function handleMouseMove(event: MouseEvent): void {
|
|
@@ -497,8 +974,17 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
497
974
|
closeButton.addEventListener('click', handleCloseClick);
|
|
498
975
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
499
976
|
window.addEventListener('keydown', handleKeydown);
|
|
977
|
+
window.addEventListener('keyup', handleKeyup);
|
|
978
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
500
979
|
window.addEventListener('mousemove', handleMouseMove);
|
|
980
|
+
window.addEventListener('resize', resizeActiveAnnotationCanvas);
|
|
981
|
+
window.addEventListener('pointermove', handleAnnotationPointerMove);
|
|
982
|
+
window.addEventListener('pointerup', handleAnnotationPointerUp);
|
|
983
|
+
window.addEventListener('pointercancel', handleAnnotationPointerUp);
|
|
984
|
+
slidesRoot.addEventListener('click', handleAnnotationClick, true);
|
|
501
985
|
slidesRoot.addEventListener('click', handleSlideClick);
|
|
986
|
+
slidesRoot.addEventListener('contextmenu', handleAnnotationContextMenu);
|
|
987
|
+
slidesRoot.addEventListener('pointerdown', handleAnnotationPointerDown);
|
|
502
988
|
slidesRoot.addEventListener('tada:slides-present', handleSlidesPresent);
|
|
503
989
|
|
|
504
990
|
return () => {
|
|
@@ -511,9 +997,19 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
511
997
|
closeButton.removeEventListener('click', handleCloseClick);
|
|
512
998
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
513
999
|
window.removeEventListener('keydown', handleKeydown);
|
|
1000
|
+
window.removeEventListener('keyup', handleKeyup);
|
|
1001
|
+
window.removeEventListener('blur', handleWindowBlur);
|
|
514
1002
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
1003
|
+
window.removeEventListener('resize', resizeActiveAnnotationCanvas);
|
|
1004
|
+
window.removeEventListener('pointermove', handleAnnotationPointerMove);
|
|
1005
|
+
window.removeEventListener('pointerup', handleAnnotationPointerUp);
|
|
1006
|
+
window.removeEventListener('pointercancel', handleAnnotationPointerUp);
|
|
1007
|
+
slidesRoot.removeEventListener('click', handleAnnotationClick, true);
|
|
515
1008
|
slidesRoot.removeEventListener('click', handleSlideClick);
|
|
1009
|
+
slidesRoot.removeEventListener('contextmenu', handleAnnotationContextMenu);
|
|
1010
|
+
slidesRoot.removeEventListener('pointerdown', handleAnnotationPointerDown);
|
|
516
1011
|
slidesRoot.removeEventListener('tada:slides-present', handleSlidesPresent);
|
|
517
1012
|
overlay.remove();
|
|
1013
|
+
eraserPreview.remove();
|
|
518
1014
|
};
|
|
519
1015
|
}
|
package/src/slides/style.scss
CHANGED
|
@@ -6,16 +6,54 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
body.is-presenting {
|
|
9
|
-
--slides-
|
|
9
|
+
--slides-annotation-cursor:
|
|
10
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(45 12 12)'%3E%3Crect x='10' y='4.8' width='4' height='13.2' rx='2' fill='white' stroke='black' stroke-width='1.5'/%3E%3Cline x1='12' y1='18' x2='12' y2='20.8' stroke='black' stroke-width='1.5' stroke-linecap='round'/%3E%3C/g%3E%3C/svg%3E")
|
|
11
|
+
6 18,
|
|
12
|
+
crosshair;
|
|
13
|
+
--slides-eraser-cursor:
|
|
14
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(-45 12 12)'%3E%3Cpath d='M6 10 Q6 8.5 7.5 8.5 H16.5 Q18 8.5 18 10 V14 Q18 15.5 16.5 15.5 H7.5 Q6 15.5 6 14 Z' fill='white' stroke='black' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cline x1='10' y1='8.5' x2='10' y2='15.5' stroke='black' stroke-width='1.5' stroke-linecap='round'/%3E%3C/g%3E%3C/svg%3E")
|
|
15
|
+
12 12,
|
|
16
|
+
cell;
|
|
17
|
+
--slides-scale: max(
|
|
18
|
+
100%,
|
|
19
|
+
min(
|
|
20
|
+
clamp(150%, 2.8vmin, 200%),
|
|
21
|
+
calc((100vw - var(--gap) - var(--gap)) / 35),
|
|
22
|
+
calc(100vh / 26.25)
|
|
23
|
+
)
|
|
24
|
+
);
|
|
10
25
|
|
|
11
26
|
overflow: hidden;
|
|
12
27
|
}
|
|
13
28
|
|
|
14
|
-
body.is-presenting.is-presentation-cursor-hidden,
|
|
15
|
-
body.is-presenting.is-presentation-cursor-hidden * {
|
|
29
|
+
body.is-presenting.is-presentation-cursor-hidden:not(.is-slides-annotating),
|
|
30
|
+
body.is-presenting.is-presentation-cursor-hidden:not(.is-slides-annotating) * {
|
|
16
31
|
cursor: none !important;
|
|
17
32
|
}
|
|
18
33
|
|
|
34
|
+
body.is-presenting.is-slides-annotating .slide-deck,
|
|
35
|
+
body.is-presenting.is-slides-annotating .slide-deck * {
|
|
36
|
+
cursor: var(--slides-annotation-cursor) !important;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
body.is-presenting.is-slides-annotating.is-slides-erasing .slide-deck,
|
|
40
|
+
body.is-presenting.is-slides-annotating.is-slides-erasing .slide-deck * {
|
|
41
|
+
cursor: var(--slides-eraser-cursor) !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
body.is-presenting [data-slides-eraser-preview] {
|
|
45
|
+
position: fixed;
|
|
46
|
+
z-index: 1002;
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
width: var(--slides-eraser-preview-size, 36px);
|
|
49
|
+
height: var(--slides-eraser-preview-size, 36px);
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
background: transparent;
|
|
52
|
+
border: 2px solid var(--bg2-color);
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
transform: translate(-50%, -50%);
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
body.is-presenting header,
|
|
20
58
|
body.is-presenting .title-and-info,
|
|
21
59
|
body.is-presenting nav.toc,
|
|
@@ -55,20 +93,30 @@ body.is-presenting .slide {
|
|
|
55
93
|
@include user-select(none);
|
|
56
94
|
|
|
57
95
|
position: fixed;
|
|
58
|
-
top:
|
|
96
|
+
top: 0;
|
|
59
97
|
left: 50%;
|
|
60
98
|
z-index: 1000;
|
|
61
99
|
box-sizing: border-box;
|
|
62
100
|
display: none;
|
|
63
|
-
width: min(
|
|
64
|
-
height:
|
|
101
|
+
width: min(calc(100vw - var(--gap) - var(--gap)), 70rem);
|
|
102
|
+
height: 100vh;
|
|
65
103
|
padding: calc(3 * var(--gap));
|
|
66
104
|
overflow: auto;
|
|
67
105
|
font-size: var(--slides-scale);
|
|
68
106
|
letter-spacing: 0.2px;
|
|
69
107
|
outline: none;
|
|
70
108
|
background: var(--bg-color);
|
|
71
|
-
transform:
|
|
109
|
+
transform: translateX(-50%);
|
|
110
|
+
|
|
111
|
+
> [data-slides-annotations] {
|
|
112
|
+
position: absolute;
|
|
113
|
+
inset: 0;
|
|
114
|
+
z-index: 1;
|
|
115
|
+
display: block;
|
|
116
|
+
width: 100%;
|
|
117
|
+
height: 100%;
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
}
|
|
72
120
|
|
|
73
121
|
> :first-child {
|
|
74
122
|
margin-top: 0;
|
package/templates/_theme.scss
CHANGED
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
// Mask icons (color-agnostic, painted via background-color: currentColor)
|
|
58
58
|
--icon-external-link: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M6 18 L20 4 M10 4 H20 V14' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
59
59
|
--icon-anchor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 28'><path d='M11 4 L8 20 M17 4 L14 20 M5 9 H21 M3 15 H19' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
60
|
-
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><
|
|
61
|
-
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='
|
|
60
|
+
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M10.8 4.2 Q12 2 13.2 4.2 L20.8 17.9 Q22 20 19.6 20 H4.4 Q2 20 3.2 17.9 Z' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='black' stroke-width='2' stroke-linecap='square'/><line x1='12' y1='16' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='square'/></svg>");
|
|
61
|
+
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='9.5' fill='none' stroke='black' stroke-width='1.5'/><line x1='12' y1='10' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='square'/><line x1='12' y1='7' x2='12' y2='7' stroke='black' stroke-width='2' stroke-linecap='square'/></svg>");
|
|
62
62
|
--icon-check: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M5 12.5 10 17 19 7' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
63
63
|
--icon-search: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7' stroke='<%= iconColor %>' stroke-width='2' fill='none'/><line x1='16.5' y1='16.5' x2='22' y2='22' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
64
64
|
--icon-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><polyline points='9 18 15 12 9 6' stroke='<%= iconColor %>' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|