@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.
@@ -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 innermost statement is executed $n - 2$ times
49
+ - when `i` is 1, the comparison is executed $n - 2$ times
52
50
  - $\dots$
53
- - when `i` is $n - 3$, the innermost statement is executed two times
54
- - when `i` is $n - 2$, the innermost statement is executed one time
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -3,7 +3,7 @@
3
3
  .question {
4
4
  --question-label-width: 1.2em;
5
5
 
6
- margin-bottom: 2rem;
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(text);
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.75em;
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.5em calc(var(--question-result-icon-size) + 1em) 0.5em 0.5em;
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(text);
188
+ @include user-select(unset);
181
189
 
182
190
  &:focus,
183
191
  &:focus-visible {
@@ -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.exitFullscreen?.();
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
- const activeSlide = slides[activeIndex];
444
- if (!activeSlide) {
529
+ if (!(event.target instanceof domView.Node)) {
445
530
  return;
446
531
  }
447
532
 
448
- if (
449
- !(event.target instanceof domView.Node) ||
450
- !activeSlide.contains(event.target)
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
- move(1);
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
  }
@@ -6,16 +6,54 @@
6
6
  }
7
7
 
8
8
  body.is-presenting {
9
- --slides-scale: clamp(150%, 3.6vmin, 250%);
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: 50%;
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(100vw, calc(100vh * 4 / 3));
64
- height: min(100vh, calc(100vw * 3 / 4));
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: translate(-50%, -50%);
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;
@@ -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'><polygon points='12,2 22,20 2,20' fill='none' stroke='black' stroke-width='1.5' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/></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='10' fill='none' stroke='black' stroke-width='1.5'/><line x1='12' y1='10' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>");
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>");