@@ -6,6 +6,7 @@ import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-
6
6
import type * as Hooks from 'preact/hooks' ;
7
7
import { DOCUMENT , WINDOW } from '../../constants' ;
8
8
import CropCornerFactory from './CropCorner' ;
9
+ import PenIconFactory from './PenIcon' ;
9
10
import { createScreenshotInputStyles } from './ScreenshotInput.css' ;
10
11
import { useTakeScreenshotFactory } from './useTakeScreenshot' ;
11
12
@@ -72,40 +73,55 @@ export function ScreenshotEditorFactory({
72
73
options,
73
74
} : FactoryParams ) : ComponentType < Props > {
74
75
const useTakeScreenshot = useTakeScreenshotFactory ( { hooks } ) ;
76
+ const CropCorner = CropCornerFactory ( { h } ) ;
77
+ const PenIcon = PenIconFactory ( { h } ) ;
75
78
76
79
return function ScreenshotEditor ( { onError } : Props ) : VNode {
77
80
const styles = hooks . useMemo ( ( ) => ( { __html : createScreenshotInputStyles ( options . styleNonce ) . innerText } ) , [ ] ) ;
78
- const CropCorner = CropCornerFactory ( { h } ) ;
79
81
80
82
const canvasContainerRef = hooks . useRef < HTMLDivElement > ( null ) ;
81
83
const cropContainerRef = hooks . useRef < HTMLDivElement > ( null ) ;
82
84
const croppingRef = hooks . useRef < HTMLCanvasElement > ( null ) ;
85
+ const annotatingRef = hooks . useRef < HTMLCanvasElement > ( null ) ;
83
86
const [ croppingRect , setCroppingRect ] = hooks . useState < Box > ( { startX : 0 , startY : 0 , endX : 0 , endY : 0 } ) ;
84
87
const [ confirmCrop , setConfirmCrop ] = hooks . useState ( false ) ;
85
88
const [ isResizing , setIsResizing ] = hooks . useState ( false ) ;
89
+ const [ isAnnotating , setIsAnnotating ] = hooks . useState ( false ) ;
86
90
87
91
hooks . useEffect ( ( ) => {
88
- WINDOW . addEventListener ( 'resize' , resizeCropper , false ) ;
92
+ WINDOW . addEventListener ( 'resize' , resize ) ;
93
+
94
+ return ( ) => {
95
+ WINDOW . removeEventListener ( 'resize' , resize ) ;
96
+ } ;
89
97
} , [ ] ) ;
90
98
91
- function resizeCropper ( ) : void {
92
- const cropper = croppingRef . current ;
93
- const imageDimensions = constructRect ( getContainedSize ( imageBuffer ) ) ;
94
- if ( cropper ) {
95
- cropper . width = imageDimensions . width * DPI ;
96
- cropper . height = imageDimensions . height * DPI ;
97
- cropper . style . width = `${ imageDimensions . width } px` ;
98
- cropper . style . height = `${ imageDimensions . height } px` ;
99
- const ctx = cropper . getContext ( '2d' ) ;
100
- if ( ctx ) {
101
- ctx . scale ( DPI , DPI ) ;
102
- }
99
+ function resizeCanvas ( canvasRef : Hooks . Ref < HTMLCanvasElement > , imageDimensions : Rect ) : void {
100
+ const canvas = canvasRef . current ;
101
+ if ( ! canvas ) {
102
+ return ;
103
103
}
104
104
105
- const cropButton = cropContainerRef . current ;
106
- if ( cropButton ) {
107
- cropButton . style . width = `${ imageDimensions . width } px` ;
108
- cropButton . style . height = `${ imageDimensions . height } px` ;
105
+ canvas . width = imageDimensions . width * DPI ;
106
+ canvas . height = imageDimensions . height * DPI ;
107
+ canvas . style . width = `${ imageDimensions . width } px` ;
108
+ canvas . style . height = `${ imageDimensions . height } px` ;
109
+ const ctx = canvas . getContext ( '2d' ) ;
110
+ if ( ctx ) {
111
+ ctx . scale ( DPI , DPI ) ;
112
+ }
113
+ }
114
+
115
+ function resize ( ) : void {
116
+ const imageDimensions = constructRect ( getContainedSize ( imageBuffer ) ) ;
117
+
118
+ resizeCanvas ( croppingRef , imageDimensions ) ;
119
+ resizeCanvas ( annotatingRef , imageDimensions ) ;
120
+
121
+ const cropContainer = cropContainerRef . current ;
122
+ if ( cropContainer ) {
123
+ cropContainer . style . width = `${ imageDimensions . width } px` ;
124
+ cropContainer . style . height = `${ imageDimensions . height } px` ;
109
125
}
110
126
111
127
setCroppingRect ( { startX : 0 , startY : 0 , endX : imageDimensions . width , endY : imageDimensions . height } ) ;
@@ -141,6 +157,7 @@ export function ScreenshotEditorFactory({
141
157
} , [ croppingRect ] ) ;
142
158
143
159
function onGrabButton ( e : Event , corner : string ) : void {
160
+ setIsAnnotating ( false ) ;
144
161
setConfirmCrop ( false ) ;
145
162
setIsResizing ( true ) ;
146
163
const handleMouseMove = makeHandleMouseMove ( corner ) ;
@@ -247,7 +264,49 @@ export function ScreenshotEditorFactory({
247
264
DOCUMENT . addEventListener ( 'mouseup' , handleMouseUp ) ;
248
265
}
249
266
250
- function submit ( ) : void {
267
+ function onAnnotateStart ( ) : void {
268
+ if ( ! isAnnotating ) {
269
+ return ;
270
+ }
271
+
272
+ const handleMouseMove = ( moveEvent : MouseEvent ) : void => {
273
+ const annotateCanvas = annotatingRef . current ;
274
+ if ( annotateCanvas ) {
275
+ const rect = annotateCanvas . getBoundingClientRect ( ) ;
276
+
277
+ const x = moveEvent . clientX - rect . x ;
278
+ const y = moveEvent . clientY - rect . y ;
279
+
280
+ const ctx = annotateCanvas . getContext ( '2d' ) ;
281
+ if ( ctx ) {
282
+ ctx . lineTo ( x , y ) ;
283
+ ctx . stroke ( ) ;
284
+ ctx . beginPath ( ) ;
285
+ ctx . moveTo ( x , y ) ;
286
+ }
287
+ }
288
+ } ;
289
+
290
+ const handleMouseUp = ( ) : void => {
291
+ const ctx = annotatingRef . current ?. getContext ( '2d' ) ;
292
+ // starts a new path so on next mouse down, the lines won't connect
293
+ if ( ctx ) {
294
+ ctx . beginPath ( ) ;
295
+ }
296
+
297
+ // draws the annotation onto the image buffer
298
+ // TODO: move this to a better place
299
+ applyAnnotation ( ) ;
300
+
301
+ DOCUMENT . removeEventListener ( 'mousemove' , handleMouseMove ) ;
302
+ DOCUMENT . removeEventListener ( 'mouseup' , handleMouseUp ) ;
303
+ } ;
304
+
305
+ DOCUMENT . addEventListener ( 'mousemove' , handleMouseMove ) ;
306
+ DOCUMENT . addEventListener ( 'mouseup' , handleMouseUp ) ;
307
+ }
308
+
309
+ function applyCrop ( ) : void {
251
310
const cutoutCanvas = DOCUMENT . createElement ( 'canvas' ) ;
252
311
const imageBox = constructRect ( getContainedSize ( imageBuffer ) ) ;
253
312
const croppingBox = constructRect ( croppingRect ) ;
@@ -277,7 +336,32 @@ export function ScreenshotEditorFactory({
277
336
imageBuffer . style . width = `${ croppingBox . width } px` ;
278
337
imageBuffer . style . height = `${ croppingBox . height } px` ;
279
338
ctx . drawImage ( cutoutCanvas , 0 , 0 ) ;
280
- resizeCropper ( ) ;
339
+ resize ( ) ;
340
+ }
341
+ }
342
+
343
+ function applyAnnotation ( ) : void {
344
+ // draw the annotations onto the image (ie "squash" the canvases)
345
+ const imageCtx = imageBuffer . getContext ( '2d' ) ;
346
+ const annotateCanvas = annotatingRef . current ;
347
+ if ( imageCtx && annotateCanvas ) {
348
+ imageCtx . drawImage (
349
+ annotateCanvas ,
350
+ 0 ,
351
+ 0 ,
352
+ annotateCanvas . width ,
353
+ annotateCanvas . height ,
354
+ 0 ,
355
+ 0 ,
356
+ imageBuffer . width ,
357
+ imageBuffer . height ,
358
+ ) ;
359
+
360
+ // clear the annotation canvas
361
+ const annotateCtx = annotateCanvas . getContext ( '2d' ) ;
362
+ if ( annotateCtx ) {
363
+ annotateCtx . clearRect ( 0 , 0 , annotateCanvas . width , annotateCanvas . height ) ;
364
+ }
281
365
}
282
366
}
283
367
@@ -303,7 +387,7 @@ export function ScreenshotEditorFactory({
303
387
( dialog . el as HTMLElement ) . style . display = 'block' ;
304
388
const container = canvasContainerRef . current ;
305
389
container ?. appendChild ( imageBuffer ) ;
306
- resizeCropper ( ) ;
390
+ resize ( ) ;
307
391
} , [ ] ) ,
308
392
onError : hooks . useCallback ( error => {
309
393
( dialog . el as HTMLElement ) . style . display = 'block' ;
@@ -314,11 +398,32 @@ export function ScreenshotEditorFactory({
314
398
return (
315
399
< div class = "editor" >
316
400
< style nonce = { options . styleNonce } dangerouslySetInnerHTML = { styles } />
401
+ { options . _experiments . annotations && (
402
+ < div class = "editor__tool-container" >
403
+ < button
404
+ class = "editor__pen-tool"
405
+ style = { {
406
+ background : isAnnotating
407
+ ? 'var(--button-primary-background, var(--accent-background))'
408
+ : 'var(--button-background, var(--background))' ,
409
+ color : isAnnotating
410
+ ? 'var(--button-primary-foreground, var(--accent-foreground))'
411
+ : 'var(--button-foreground, var(--foreground))' ,
412
+ } }
413
+ onClick = { e => {
414
+ e . preventDefault ( ) ;
415
+ setIsAnnotating ( ! isAnnotating ) ;
416
+ } }
417
+ >
418
+ < PenIcon />
419
+ </ button >
420
+ </ div >
421
+ ) }
317
422
< div class = "editor__canvas-container" ref = { canvasContainerRef } >
318
- < div class = "editor__crop-container" style = { { position : 'absolute' , zIndex : 1 } } ref = { cropContainerRef } >
423
+ < div class = "editor__crop-container" style = { { zIndex : isAnnotating ? 1 : 2 } } ref = { cropContainerRef } >
319
424
< canvas
320
425
onMouseDown = { onDragStart }
321
- style = { { position : 'absolute' , cursor : confirmCrop ? 'move' : 'auto' } }
426
+ style = { { cursor : confirmCrop ? 'move' : 'auto' } }
322
427
ref = { croppingRef }
323
428
> </ canvas >
324
429
< CropCorner
@@ -373,7 +478,7 @@ export function ScreenshotEditorFactory({
373
478
< button
374
479
onClick = { e => {
375
480
e . preventDefault ( ) ;
376
- submit ( ) ;
481
+ applyCrop ( ) ;
377
482
setConfirmCrop ( false ) ;
378
483
} }
379
484
class = "btn btn--primary"
@@ -382,6 +487,12 @@ export function ScreenshotEditorFactory({
382
487
</ button >
383
488
</ div >
384
489
</ div >
490
+ < canvas
491
+ class = "editor__annotation"
492
+ onMouseDown = { onAnnotateStart }
493
+ style = { { zIndex : isAnnotating ? '2' : '1' } }
494
+ ref = { annotatingRef }
495
+ > </ canvas >
385
496
</ div >
386
497
</ div >
387
498
) ;
0 commit comments