@@ -6,6 +6,7 @@ import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-
66import type * as Hooks from 'preact/hooks' ;
77import { DOCUMENT , WINDOW } from '../../constants' ;
88import CropCornerFactory from './CropCorner' ;
9+ import PenIconFactory from './PenIcon' ;
910import { createScreenshotInputStyles } from './ScreenshotInput.css' ;
1011import { useTakeScreenshotFactory } from './useTakeScreenshot' ;
1112
@@ -72,40 +73,55 @@ export function ScreenshotEditorFactory({
7273 options,
7374} : FactoryParams ) : ComponentType < Props > {
7475 const useTakeScreenshot = useTakeScreenshotFactory ( { hooks } ) ;
76+ const CropCorner = CropCornerFactory ( { h } ) ;
77+ const PenIcon = PenIconFactory ( { h } ) ;
7578
7679 return function ScreenshotEditor ( { onError } : Props ) : VNode {
7780 const styles = hooks . useMemo ( ( ) => ( { __html : createScreenshotInputStyles ( options . styleNonce ) . innerText } ) , [ ] ) ;
78- const CropCorner = CropCornerFactory ( { h } ) ;
7981
8082 const canvasContainerRef = hooks . useRef < HTMLDivElement > ( null ) ;
8183 const cropContainerRef = hooks . useRef < HTMLDivElement > ( null ) ;
8284 const croppingRef = hooks . useRef < HTMLCanvasElement > ( null ) ;
85+ const annotatingRef = hooks . useRef < HTMLCanvasElement > ( null ) ;
8386 const [ croppingRect , setCroppingRect ] = hooks . useState < Box > ( { startX : 0 , startY : 0 , endX : 0 , endY : 0 } ) ;
8487 const [ confirmCrop , setConfirmCrop ] = hooks . useState ( false ) ;
8588 const [ isResizing , setIsResizing ] = hooks . useState ( false ) ;
89+ const [ isAnnotating , setIsAnnotating ] = hooks . useState ( false ) ;
8690
8791 hooks . useEffect ( ( ) => {
88- WINDOW . addEventListener ( 'resize' , resizeCropper , false ) ;
92+ WINDOW . addEventListener ( 'resize' , resize ) ;
93+
94+ return ( ) => {
95+ WINDOW . removeEventListener ( 'resize' , resize ) ;
96+ } ;
8997 } , [ ] ) ;
9098
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 ;
103103 }
104104
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` ;
109125 }
110126
111127 setCroppingRect ( { startX : 0 , startY : 0 , endX : imageDimensions . width , endY : imageDimensions . height } ) ;
@@ -141,6 +157,7 @@ export function ScreenshotEditorFactory({
141157 } , [ croppingRect ] ) ;
142158
143159 function onGrabButton ( e : Event , corner : string ) : void {
160+ setIsAnnotating ( false ) ;
144161 setConfirmCrop ( false ) ;
145162 setIsResizing ( true ) ;
146163 const handleMouseMove = makeHandleMouseMove ( corner ) ;
@@ -247,7 +264,49 @@ export function ScreenshotEditorFactory({
247264 DOCUMENT . addEventListener ( 'mouseup' , handleMouseUp ) ;
248265 }
249266
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 {
251310 const cutoutCanvas = DOCUMENT . createElement ( 'canvas' ) ;
252311 const imageBox = constructRect ( getContainedSize ( imageBuffer ) ) ;
253312 const croppingBox = constructRect ( croppingRect ) ;
@@ -277,7 +336,32 @@ export function ScreenshotEditorFactory({
277336 imageBuffer . style . width = `${ croppingBox . width } px` ;
278337 imageBuffer . style . height = `${ croppingBox . height } px` ;
279338 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+ }
281365 }
282366 }
283367
@@ -303,7 +387,7 @@ export function ScreenshotEditorFactory({
303387 ( dialog . el as HTMLElement ) . style . display = 'block' ;
304388 const container = canvasContainerRef . current ;
305389 container ?. appendChild ( imageBuffer ) ;
306- resizeCropper ( ) ;
390+ resize ( ) ;
307391 } , [ ] ) ,
308392 onError : hooks . useCallback ( error => {
309393 ( dialog . el as HTMLElement ) . style . display = 'block' ;
@@ -314,11 +398,32 @@ export function ScreenshotEditorFactory({
314398 return (
315399 < div class = "editor" >
316400 < 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+ ) }
317422 < 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 } >
319424 < canvas
320425 onMouseDown = { onDragStart }
321- style = { { position : 'absolute' , cursor : confirmCrop ? 'move' : 'auto' } }
426+ style = { { cursor : confirmCrop ? 'move' : 'auto' } }
322427 ref = { croppingRef }
323428 > </ canvas >
324429 < CropCorner
@@ -373,7 +478,7 @@ export function ScreenshotEditorFactory({
373478 < button
374479 onClick = { e => {
375480 e . preventDefault ( ) ;
376- submit ( ) ;
481+ applyCrop ( ) ;
377482 setConfirmCrop ( false ) ;
378483 } }
379484 class = "btn btn--primary"
@@ -382,6 +487,12 @@ export function ScreenshotEditorFactory({
382487 </ button >
383488 </ div >
384489 </ div >
490+ < canvas
491+ class = "editor__annotation"
492+ onMouseDown = { onAnnotateStart }
493+ style = { { zIndex : isAnnotating ? '2' : '1' } }
494+ ref = { annotatingRef }
495+ > </ canvas >
385496 </ div >
386497 </ div >
387498 ) ;
0 commit comments