@abreen/tada 1.15.1 → 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/package.json +1 -1
- package/src/question/style.scss +2 -2
- package/src/slides/index.ts +486 -0
- package/src/slides/style.scss +51 -3
package/package.json
CHANGED
package/src/question/style.scss
CHANGED
|
@@ -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 {
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
.question-multiple-choice-option {
|
|
186
186
|
cursor: unset;
|
|
187
187
|
|
|
188
|
-
@include user-select(
|
|
188
|
+
@include user-select(unset);
|
|
189
189
|
|
|
190
190
|
&:focus,
|
|
191
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 {
|
|
@@ -247,6 +305,9 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
247
305
|
isPresenting = false;
|
|
248
306
|
activeIndex = -1;
|
|
249
307
|
presentationMode = null;
|
|
308
|
+
isShiftPressed = false;
|
|
309
|
+
setAnnotationMode(false);
|
|
310
|
+
clearAnnotationCanvases();
|
|
250
311
|
hidePresentationControls();
|
|
251
312
|
showPresentationCursor();
|
|
252
313
|
document.body.classList.remove('is-presenting');
|
|
@@ -289,6 +350,7 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
289
350
|
function enterPresentation(mode: PresentationMode, slideIndex = 0): void {
|
|
290
351
|
isPresenting = true;
|
|
291
352
|
presentationMode = mode;
|
|
353
|
+
setAnnotationMode(false);
|
|
292
354
|
hidePresentationControls();
|
|
293
355
|
showPresentationCursor();
|
|
294
356
|
document.body.classList.add('is-presenting');
|
|
@@ -428,6 +490,12 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
428
490
|
return;
|
|
429
491
|
}
|
|
430
492
|
|
|
493
|
+
if (event.key === 'Shift') {
|
|
494
|
+
isShiftPressed = true;
|
|
495
|
+
setEraserMode(isAnnotating);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
431
499
|
if (
|
|
432
500
|
(event.key === 'ArrowRight' ||
|
|
433
501
|
event.key === 'ArrowLeft' ||
|
|
@@ -465,6 +533,405 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
465
533
|
move(1);
|
|
466
534
|
}
|
|
467
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) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
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();
|
|
933
|
+
}
|
|
934
|
+
|
|
468
935
|
function handleMouseMove(event: MouseEvent): void {
|
|
469
936
|
if (!isPresenting) {
|
|
470
937
|
return;
|
|
@@ -507,8 +974,17 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
507
974
|
closeButton.addEventListener('click', handleCloseClick);
|
|
508
975
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
509
976
|
window.addEventListener('keydown', handleKeydown);
|
|
977
|
+
window.addEventListener('keyup', handleKeyup);
|
|
978
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
510
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);
|
|
511
985
|
slidesRoot.addEventListener('click', handleSlideClick);
|
|
986
|
+
slidesRoot.addEventListener('contextmenu', handleAnnotationContextMenu);
|
|
987
|
+
slidesRoot.addEventListener('pointerdown', handleAnnotationPointerDown);
|
|
512
988
|
slidesRoot.addEventListener('tada:slides-present', handleSlidesPresent);
|
|
513
989
|
|
|
514
990
|
return () => {
|
|
@@ -521,9 +997,19 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
521
997
|
closeButton.removeEventListener('click', handleCloseClick);
|
|
522
998
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
523
999
|
window.removeEventListener('keydown', handleKeydown);
|
|
1000
|
+
window.removeEventListener('keyup', handleKeyup);
|
|
1001
|
+
window.removeEventListener('blur', handleWindowBlur);
|
|
524
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);
|
|
525
1008
|
slidesRoot.removeEventListener('click', handleSlideClick);
|
|
1009
|
+
slidesRoot.removeEventListener('contextmenu', handleAnnotationContextMenu);
|
|
1010
|
+
slidesRoot.removeEventListener('pointerdown', handleAnnotationPointerDown);
|
|
526
1011
|
slidesRoot.removeEventListener('tada:slides-present', handleSlidesPresent);
|
|
527
1012
|
overlay.remove();
|
|
1013
|
+
eraserPreview.remove();
|
|
528
1014
|
};
|
|
529
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,
|
|
@@ -70,6 +108,16 @@ body.is-presenting .slide {
|
|
|
70
108
|
background: var(--bg-color);
|
|
71
109
|
transform: translateX(-50%);
|
|
72
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
|
+
}
|
|
120
|
+
|
|
73
121
|
> :first-child {
|
|
74
122
|
margin-top: 0;
|
|
75
123
|
}
|