1- import { type CSSProperties , type PointerEvent , useRef , useState } from "react" ;
1+ import { type CSSProperties , type PointerEvent , useEffect , useRef , useState } from "react" ;
22import { Rnd } from "react-rnd" ;
3+ import {
4+ getBlurOverlayColor ,
5+ getMosaicGridOverlayColor ,
6+ getNormalizedMosaicBlockSize ,
7+ } from "@/lib/blurEffects" ;
38import { cn } from "@/lib/utils" ;
49import { getArrowComponent } from "./ArrowSvgs" ;
510import {
611 type AnnotationRegion ,
712 type BlurData ,
13+ DEFAULT_BLUR_BLOCK_SIZE ,
814 DEFAULT_BLUR_DATA ,
915 DEFAULT_BLUR_INTENSITY ,
1016} from "./types" ;
1117
1218const FREEHAND_POINT_THRESHOLD = 1 ;
19+ type PreviewCanvasSource = {
20+ width : number ;
21+ height : number ;
22+ clientWidth ?: number ;
23+ clientHeight ?: number ;
24+ } ;
1325
1426function buildBlurPolygonClipPath ( points : Array < { x : number ; y : number } > ) {
1527 if ( points . length < 3 ) return undefined ;
@@ -36,6 +48,8 @@ interface AnnotationOverlayProps {
3648 onClick : ( id : string ) => void ;
3749 zIndex : number ;
3850 isSelectedBoost : boolean ; // Boost z-index when selected for easy editing
51+ previewSourceCanvas ?: PreviewCanvasSource | null ;
52+ previewFrameVersion ?: number ;
3953}
4054
4155export function AnnotationOverlay ( {
@@ -50,11 +64,13 @@ export function AnnotationOverlay({
5064 onClick,
5165 zIndex,
5266 isSelectedBoost,
67+ previewSourceCanvas,
68+ previewFrameVersion,
5369} : AnnotationOverlayProps ) {
54- const x = ( annotation . position . x / 100 ) * containerWidth ;
55- const y = ( annotation . position . y / 100 ) * containerHeight ;
56- const width = ( annotation . size . width / 100 ) * containerWidth ;
57- const height = ( annotation . size . height / 100 ) * containerHeight ;
70+ const committedX = ( annotation . position . x / 100 ) * containerWidth ;
71+ const committedY = ( annotation . position . y / 100 ) * containerHeight ;
72+ const committedWidth = ( annotation . size . width / 100 ) * containerWidth ;
73+ const committedHeight = ( annotation . size . height / 100 ) * containerHeight ;
5874 const blurShape = annotation . type === "blur" ? ( annotation . blurData ?. shape ?? "rectangle" ) : null ;
5975 const isSelectedFreehandBlur = isSelected && blurShape === "freehand" ;
6076 const isDraggingRef = useRef ( false ) ;
@@ -65,6 +81,108 @@ export function AnnotationOverlay({
6581 [ ] ,
6682 ) ;
6783 const [ livePointerPoint , setLivePointerPoint ] = useState < { x : number ; y : number } | null > ( null ) ;
84+ const mosaicCanvasRef = useRef < HTMLCanvasElement | null > ( null ) ;
85+ const blurType = annotation . type === "blur" ? ( annotation . blurData ?. type ?? "blur" ) : "blur" ;
86+ const blurOverlayColor =
87+ annotation . type === "blur" ? getBlurOverlayColor ( annotation . blurData ) : "" ;
88+ const mosaicGridOverlayColor =
89+ annotation . type === "blur" ? getMosaicGridOverlayColor ( annotation . blurData ) : "" ;
90+ const [ liveRect , setLiveRect ] = useState ( {
91+ x : committedX ,
92+ y : committedY ,
93+ width : committedWidth ,
94+ height : committedHeight ,
95+ } ) ;
96+
97+ useEffect ( ( ) => {
98+ setLiveRect ( {
99+ x : committedX ,
100+ y : committedY ,
101+ width : committedWidth ,
102+ height : committedHeight ,
103+ } ) ;
104+ } , [ committedHeight , committedWidth , committedX , committedY ] ) ;
105+
106+ const { x, y, width, height } = liveRect ;
107+
108+ useEffect ( ( ) => {
109+ if ( annotation . type !== "blur" || blurType !== "mosaic" ) {
110+ return ;
111+ }
112+ void previewFrameVersion ;
113+
114+ const canvas = mosaicCanvasRef . current ;
115+ const sourceCanvas = previewSourceCanvas ;
116+ if ( ! canvas || ! sourceCanvas ) {
117+ return ;
118+ }
119+
120+ const sourceWidth = sourceCanvas . width ;
121+ const sourceHeight = sourceCanvas . height ;
122+ const sourceClientWidth = sourceCanvas . clientWidth || containerWidth || sourceWidth ;
123+ const sourceClientHeight = sourceCanvas . clientHeight || containerHeight || sourceHeight ;
124+ if (
125+ sourceWidth <= 0 ||
126+ sourceHeight <= 0 ||
127+ sourceClientWidth <= 0 ||
128+ sourceClientHeight <= 0
129+ ) {
130+ return ;
131+ }
132+
133+ const drawWidth = Math . max ( 1 , Math . round ( width ) ) ;
134+ const drawHeight = Math . max ( 1 , Math . round ( height ) ) ;
135+ if ( drawWidth <= 0 || drawHeight <= 0 ) {
136+ return ;
137+ }
138+
139+ canvas . width = drawWidth ;
140+ canvas . height = drawHeight ;
141+
142+ const context = canvas . getContext ( "2d" , { willReadFrequently : true } ) ;
143+ if ( ! context ) {
144+ return ;
145+ }
146+
147+ const scaleX = sourceWidth / sourceClientWidth ;
148+ const scaleY = sourceHeight / sourceClientHeight ;
149+ const sourceX = Math . max ( 0 , Math . floor ( x * scaleX ) ) ;
150+ const sourceY = Math . max ( 0 , Math . floor ( y * scaleY ) ) ;
151+ const sourceSampleWidth = Math . max ( 1 , Math . ceil ( drawWidth * scaleX ) ) ;
152+ const sourceSampleHeight = Math . max ( 1 , Math . ceil ( drawHeight * scaleY ) ) ;
153+ const clampedSampleWidth = Math . max ( 1 , Math . min ( sourceSampleWidth , sourceWidth - sourceX ) ) ;
154+ const clampedSampleHeight = Math . max ( 1 , Math . min ( sourceSampleHeight , sourceHeight - sourceY ) ) ;
155+ const blockSize = getNormalizedMosaicBlockSize ( annotation . blurData ) ;
156+ const downscaledWidth = Math . max ( 1 , Math . round ( drawWidth / blockSize ) ) ;
157+ const downscaledHeight = Math . max ( 1 , Math . round ( drawHeight / blockSize ) ) ;
158+ canvas . width = downscaledWidth ;
159+ canvas . height = downscaledHeight ;
160+
161+ context . clearRect ( 0 , 0 , downscaledWidth , downscaledHeight ) ;
162+ context . imageSmoothingEnabled = true ;
163+ context . drawImage (
164+ sourceCanvas as CanvasImageSource ,
165+ sourceX ,
166+ sourceY ,
167+ clampedSampleWidth ,
168+ clampedSampleHeight ,
169+ 0 ,
170+ 0 ,
171+ downscaledWidth ,
172+ downscaledHeight ,
173+ ) ;
174+ } , [
175+ annotation ,
176+ blurType ,
177+ containerHeight ,
178+ containerWidth ,
179+ height ,
180+ previewFrameVersion ,
181+ previewSourceCanvas ,
182+ width ,
183+ x ,
184+ y ,
185+ ] ) ;
68186
69187 const renderArrow = ( ) => {
70188 const direction = annotation . figureData ?. arrowDirection || "right" ;
@@ -240,6 +358,10 @@ export function AnnotationOverlay({
240358 1 ,
241359 Math . round ( annotation . blurData ?. intensity ?? DEFAULT_BLUR_INTENSITY ) ,
242360 ) ;
361+ const blockSize = Math . max (
362+ 1 ,
363+ Math . round ( annotation . blurData ?. blockSize ?? DEFAULT_BLUR_BLOCK_SIZE ) ,
364+ ) ;
243365 const activeFreehandPoints =
244366 shape === "freehand"
245367 ? isFreehandDrawing
@@ -292,12 +414,43 @@ export function AnnotationOverlay({
292414 className = "absolute inset-0"
293415 style = { {
294416 ...shapeMaskStyle ,
295- backdropFilter : `blur(${ blurIntensity } px)` ,
296- WebkitBackdropFilter : `blur(${ blurIntensity } px)` ,
297- backgroundColor : "rgba(255, 255, 255, 0.02)" ,
417+ backdropFilter : blurType === "mosaic" ? "none" : `blur(${ blurIntensity } px)` ,
418+ WebkitBackdropFilter : blurType === "mosaic" ? "none" : `blur(${ blurIntensity } px)` ,
419+ backgroundColor : blurOverlayColor ,
298420 opacity : shouldShowFreehandBlurFill ? 1 : 0 ,
299421 } }
300422 />
423+ { blurType === "mosaic" && shouldShowFreehandBlurFill && (
424+ < canvas
425+ ref = { mosaicCanvasRef }
426+ className = "absolute inset-0 w-full h-full"
427+ style = { {
428+ ...shapeMaskStyle ,
429+ imageRendering : "pixelated" ,
430+ } }
431+ />
432+ ) }
433+ { blurType === "mosaic" && shouldShowFreehandBlurFill && (
434+ < div
435+ className = "absolute inset-0 pointer-events-none"
436+ style = { {
437+ ...shapeMaskStyle ,
438+ backgroundColor : blurOverlayColor ,
439+ } }
440+ />
441+ ) }
442+ { blurType === "mosaic" && (
443+ < div
444+ className = "absolute inset-0 pointer-events-none"
445+ style = { {
446+ ...shapeMaskStyle ,
447+ backgroundImage : `linear-gradient(${ mosaicGridOverlayColor } 1px, transparent 1px), linear-gradient(90deg, ${ mosaicGridOverlayColor } 1px, transparent 1px)` ,
448+ backgroundSize : `${ blockSize } px ${ blockSize } px` ,
449+ mixBlendMode : "screen" ,
450+ opacity : 0.35 ,
451+ } }
452+ />
453+ ) }
301454 { isSelected && shape !== "freehand" && (
302455 < div
303456 className = "absolute inset-0 pointer-events-none border-2 border-[#34B27B]/80"
@@ -354,7 +507,19 @@ export function AnnotationOverlay({
354507 onDragStart = { ( ) => {
355508 isDraggingRef . current = true ;
356509 } }
510+ onDrag = { ( _e , d ) => {
511+ setLiveRect ( ( prev ) => ( {
512+ ...prev ,
513+ x : d . x ,
514+ y : d . y ,
515+ } ) ) ;
516+ } }
357517 onDragStop = { ( _e , d ) => {
518+ setLiveRect ( ( prev ) => ( {
519+ ...prev ,
520+ x : d . x ,
521+ y : d . y ,
522+ } ) ) ;
358523 const xPercent = ( d . x / containerWidth ) * 100 ;
359524 const yPercent = ( d . y / containerHeight ) * 100 ;
360525 onPositionChange ( annotation . id , { x : xPercent , y : yPercent } ) ;
@@ -364,7 +529,21 @@ export function AnnotationOverlay({
364529 isDraggingRef . current = false ;
365530 } , 100 ) ;
366531 } }
532+ onResize = { ( _e , _direction , ref , _delta , position ) => {
533+ setLiveRect ( {
534+ x : position . x ,
535+ y : position . y ,
536+ width : ref . offsetWidth ,
537+ height : ref . offsetHeight ,
538+ } ) ;
539+ } }
367540 onResizeStop = { ( _e , _direction , ref , _delta , position ) => {
541+ setLiveRect ( {
542+ x : position . x ,
543+ y : position . y ,
544+ width : ref . offsetWidth ,
545+ height : ref . offsetHeight ,
546+ } ) ;
368547 const xPercent = ( position . x / containerWidth ) * 100 ;
369548 const yPercent = ( position . y / containerHeight ) * 100 ;
370549 const widthPercent = ( ref . offsetWidth / containerWidth ) * 100 ;
0 commit comments