1+ import { createElementBounds } from "@solid-primitives/bounds" ;
2+ import { createEventListenerMap } from "@solid-primitives/event-listener" ;
3+ import { createRoot , createSignal , For , Show } from "solid-js" ;
4+ import { cx } from "cva" ;
5+ import { useEditorContext } from "./context" ;
6+
7+
8+ interface BlurRectangleProps {
9+ rect : { x : number ; y : number ; width : number ; height : number } ;
10+ style : { left : string ; top : string ; width : string ; height : string ; filter ?: string } ;
11+ onUpdate : ( rect : { x : number ; y : number ; width : number ; height : number } ) => void ;
12+ containerBounds : { width ?: number | null ; height ?: number | null } ;
13+ blurAmount : number ;
14+ isEditing : boolean ;
15+ }
16+
17+ export function BlurOverlay ( ) {
18+ const { project, setProject, editorState } = useEditorContext ( ) ;
19+
20+ const [ canvasContainerRef , setCanvasContainerRef ] = createSignal < HTMLDivElement > ( ) ;
21+ const containerBounds = createElementBounds ( canvasContainerRef ) ;
22+
23+ const currentTime = ( ) => editorState . previewTime ?? editorState . playbackTime ?? 0 ;
24+
25+ const activeBlurSegmentsWithIndex = ( ) => {
26+ return ( project . timeline ?. blurSegments || [ ] ) . map ( ( segment , index ) => ( { segment, index } ) ) . filter (
27+ ( { segment } ) => currentTime ( ) >= segment . start && currentTime ( ) <= segment . end
28+ ) ;
29+ } ;
30+
31+ const updateBlurRect = ( index : number , rect : { x : number ; y : number ; width : number ; height : number } ) => {
32+ setProject ( "timeline" , "blurSegments" , index , "rect" , rect ) ;
33+ } ;
34+
35+ const isSelected = ( index : number ) => {
36+ const selection = editorState . timeline . selection ;
37+ return selection ?. type === "blur" && selection . index === index ;
38+ } ;
39+
40+ return (
41+ < div
42+ ref = { setCanvasContainerRef }
43+ class = "absolute inset-0 pointer-events-none"
44+ >
45+ < For each = { activeBlurSegmentsWithIndex ( ) } >
46+ { ( { segment, index } ) => {
47+ // Convert normalized coordinates to pixel coordinates
48+ const rectStyle = ( ) => {
49+ const containerWidth = containerBounds . width ?? 1 ;
50+ const containerHeight = containerBounds . height ?? 1 ;
51+
52+ return {
53+ left : `${ segment . rect . x * containerWidth } px` ,
54+ top : `${ segment . rect . y * containerHeight } px` ,
55+ width : `${ segment . rect . width * containerWidth } px` ,
56+ height : `${ segment . rect . height * containerHeight } px` ,
57+ } ;
58+ } ;
59+
60+ return (
61+ < BlurRectangle
62+ rect = { segment . rect }
63+ style = { rectStyle ( ) }
64+ blurAmount = { segment . blur_amount || 0 }
65+ onUpdate = { ( newRect ) => updateBlurRect ( index , newRect ) }
66+ containerBounds = { containerBounds }
67+ isEditing = { isSelected ( index ) }
68+ />
69+ ) ;
70+ } }
71+ </ For >
72+ </ div >
73+ ) ;
74+ }
75+
76+
77+
78+ function BlurRectangle ( props : BlurRectangleProps ) {
79+ const handleMouseDown = ( e : MouseEvent , action : 'move' | 'resize' , corner ?: string ) => {
80+ e . preventDefault ( ) ;
81+ e . stopPropagation ( ) ;
82+
83+ const containerWidth = props . containerBounds . width ?? 1 ;
84+ const containerHeight = props . containerBounds . height ?? 1 ;
85+
86+ const startX = e . clientX ;
87+ const startY = e . clientY ;
88+ const startRect = { ...props . rect } ;
89+
90+ createRoot ( ( dispose ) => {
91+ createEventListenerMap ( window , {
92+ mousemove : ( moveEvent : MouseEvent ) => {
93+ const deltaX = ( moveEvent . clientX - startX ) / containerWidth ;
94+ const deltaY = ( moveEvent . clientY - startY ) / containerHeight ;
95+
96+ let newRect = { ...startRect } ;
97+
98+ if ( action === 'move' ) {
99+ newRect . x = Math . max ( 0 , Math . min ( 1 - newRect . width , startRect . x + deltaX ) ) ;
100+ newRect . y = Math . max ( 0 , Math . min ( 1 - newRect . height , startRect . y + deltaY ) ) ;
101+ } else if ( action === 'resize' ) {
102+ switch ( corner ) {
103+ case 'nw' : // Northwest corner
104+ newRect . x = Math . max ( 0 , startRect . x + deltaX ) ;
105+ newRect . y = Math . max ( 0 , startRect . y + deltaY ) ;
106+ newRect . width = startRect . width - deltaX ;
107+ newRect . height = startRect . height - deltaY ;
108+ break ;
109+ case 'ne' : // Northeast corner
110+ newRect . y = Math . max ( 0 , startRect . y + deltaY ) ;
111+ newRect . width = startRect . width + deltaX ;
112+ newRect . height = startRect . height - deltaY ;
113+ break ;
114+ case 'sw' : // Southwest corner
115+ newRect . x = Math . max ( 0 , startRect . x + deltaX ) ;
116+ newRect . width = startRect . width - deltaX ;
117+ newRect . height = startRect . height + deltaY ;
118+ break ;
119+ case 'se' : // Southeast corner
120+ newRect . width = startRect . width + deltaX ;
121+ newRect . height = startRect . height + deltaY ;
122+ break ;
123+ }
124+
125+ // Ensure minimum size
126+ newRect . width = Math . max ( 0.05 , newRect . width ) ;
127+ newRect . height = Math . max ( 0.05 , newRect . height ) ;
128+
129+ // Ensure within bounds
130+ newRect . x = Math . max ( 0 , Math . min ( 1 - newRect . width , newRect . x ) ) ;
131+ newRect . y = Math . max ( 0 , Math . min ( 1 - newRect . height , newRect . y ) ) ;
132+ newRect . width = Math . min ( 1 - newRect . x , newRect . width ) ;
133+ newRect . height = Math . min ( 1 - newRect . y , newRect . height ) ;
134+ }
135+
136+ props . onUpdate ( newRect ) ;
137+ } ,
138+ mouseup : ( ) => {
139+ dispose ( ) ;
140+ } ,
141+ } ) ;
142+ } ) ;
143+ } ;
144+
145+ return (
146+ < div
147+ class = { cx (
148+ "absolute" ,
149+ props . isEditing ? "pointer-events-auto border-2 border-blue-400 bg-blue-400/20" : "pointer-events-none border-none bg-transparent"
150+ ) }
151+ style = { {
152+ ...props . style ,
153+ "backdrop-filter" : `blur(${ props . blurAmount } px)` ,
154+ "-webkit-backdrop-filter" : `blur(${ props . blurAmount } px)` , // Fallback for WebKit browsers
155+ } }
156+ >
157+ < Show when = { props . isEditing } >
158+ { /* Main draggable area */ }
159+ < div
160+ class = "absolute inset-0 cursor-move"
161+ onMouseDown = { ( e ) => handleMouseDown ( e , 'move' ) }
162+ />
163+
164+ { /* Resize handles */ }
165+ < div
166+ class = "absolute -top-1 -left-1 w-3 h-3 bg-blue-400 border border-white cursor-nw-resize rounded-full"
167+ onMouseDown = { ( e ) => handleMouseDown ( e , 'resize' , 'nw' ) }
168+ />
169+ < div
170+ class = "absolute -top-1 -right-1 w-3 h-3 bg-blue-400 border border-white cursor-ne-resize rounded-full"
171+ onMouseDown = { ( e ) => handleMouseDown ( e , 'resize' , 'ne' ) }
172+ />
173+ < div
174+ class = "absolute -bottom-1 -left-1 w-3 h-3 bg-blue-400 border border-white cursor-sw-resize rounded-full"
175+ onMouseDown = { ( e ) => handleMouseDown ( e , 'resize' , 'sw' ) }
176+ />
177+ < div
178+ class = "absolute -bottom-1 -right-1 w-3 h-3 bg-blue-400 border border-white cursor-se-resize rounded-full"
179+ onMouseDown = { ( e ) => handleMouseDown ( e , 'resize' , 'se' ) }
180+ />
181+
182+ { /* Center label */ }
183+ { /* <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
184+ <div class="px-2 py-1 bg-blue-500 text-white text-xs rounded shadow-lg">
185+ <IconCapBlur class="inline w-3 h-3 mr-1" />
186+ Blur Area
187+ </div>
188+ </div> */ }
189+ </ Show >
190+ </ div >
191+ ) ;
192+ }
0 commit comments