@@ -54,6 +54,7 @@ type WebSocketConnection = {
5454 connected : boolean ;
5555} ;
5656
57+ // NOTE: Comments in this Canvas Engine are not AI generated. This are for my personal understanding.
5758export class CanvasEngine {
5859 private canvas : HTMLCanvasElement ;
5960 private ctx : CanvasRenderingContext2D ;
@@ -126,6 +127,7 @@ export class CanvasEngine {
126127 private remoteClickIndicators : Map < string , number > = new Map ( ) ;
127128
128129 private currentTheme : "light" | "dark" | null = null ;
130+ private onLiveUpdateFromSelection ?: ( shape : Shape ) => void ;
129131
130132 constructor (
131133 canvas : HTMLCanvasElement ,
@@ -176,6 +178,12 @@ export class CanvasEngine {
176178 ) ;
177179 }
178180 } ) ;
181+ this . SelectionController . setOnLiveUpdate ( ( shape ) => {
182+ this . streamShapeUpdate ( shape ) ;
183+ } ) ;
184+ this . SelectionController . setOnDragOrResizeCursorMove ( ( x , y ) => {
185+ this . sendCursorMove ( x , y ) ;
186+ } ) ;
179187 if ( ! this . isStandalone && this . token && this . roomId ) {
180188 console . log ( "✅Connecting to WebSocket…" ) ;
181189 this . connectWebSocket ( ) ;
@@ -332,11 +340,6 @@ export class CanvasEngine {
332340 this . remoteStreamingShapes . set ( streamKey , streamedShape ) ;
333341 const userConnKey = `${ data . userId } -${ data . connectionId } ` ;
334342 this . remoteClickIndicators . set ( userConnKey , Date . now ( ) ) ;
335- // console.log(
336- // "this.remoteClickIndicators = ",
337- // this.remoteClickIndicators
338- // );
339-
340343 this . clearCanvas ( ) ;
341344 } catch ( err ) {
342345 console . error ( "Error handling streamed shape:" , err ) ;
@@ -378,6 +381,29 @@ export class CanvasEngine {
378381 }
379382 break ;
380383
384+ case WsDataType . STREAM_UPDATE :
385+ if (
386+ data . userId !== this . userId &&
387+ data . connectionId &&
388+ data . message
389+ ) {
390+ const decrypted = await decryptData (
391+ data . message ,
392+ this . encryptionKey !
393+ ) ;
394+ const streamedShape = JSON . parse ( decrypted ) ;
395+ const streamKey = getStreamKey ( {
396+ userId : data . userId ,
397+ connectionId : data . connectionId ,
398+ shapeId : streamedShape . id ,
399+ } ) ;
400+ this . remoteStreamingShapes . set ( streamKey , streamedShape ) ;
401+ const userConnKey = `${ data . userId } -${ data . connectionId } ` ;
402+ this . remoteClickIndicators . set ( userConnKey , Date . now ( ) ) ;
403+ this . clearCanvas ( ) ;
404+ }
405+ break ;
406+
381407 case WsDataType . DRAW :
382408 case WsDataType . UPDATE :
383409 if (
@@ -533,6 +559,48 @@ export class CanvasEngine {
533559 } , this . streamingUpdateInterval ) ;
534560 }
535561
562+ private streamShapeUpdate ( shape : Shape ) {
563+ if ( ! this . isConnected || this . isStandalone ) return ;
564+ if ( this . streamingThrottleTimeout !== null ) return ;
565+
566+ this . streamingThrottleTimeout = window . setTimeout ( ( ) => {
567+ if ( this . socket ?. readyState === WebSocket . OPEN && this . roomId ) {
568+ const message = {
569+ type : WsDataType . STREAM_UPDATE ,
570+ id : shape . id ,
571+ message : shape ,
572+ roomId : this . roomId ,
573+ userId : this . userId ! ,
574+ userName : this . userName ! ,
575+ timestamp : new Date ( ) . toISOString ( ) ,
576+ connectionId : this . connectionId ,
577+ } ;
578+
579+ this . sendMessage ?.( JSON . stringify ( message ) ) . catch ( ( e ) => {
580+ console . error ( "Error streaming shape update" , e ) ;
581+ } ) ;
582+ }
583+ this . streamingThrottleTimeout = null ;
584+ } , this . streamingUpdateInterval ) ;
585+ }
586+
587+ private sendCursorMove ( x : number , y : number ) {
588+ if ( ! this . isStandalone && this . isConnected ) {
589+ const message = {
590+ type : WsDataType . CURSOR_MOVE ,
591+ roomId : this . roomId ,
592+ userId : this . userId ! ,
593+ userName : this . userName ! ,
594+ connectionId : this . connectionId ,
595+ message : JSON . stringify ( { x, y } ) ,
596+ } ;
597+
598+ if ( this . socket ?. readyState === WebSocket . OPEN ) {
599+ this . socket . send ( JSON . stringify ( message ) ) ;
600+ }
601+ }
602+ }
603+
536604 async init ( ) {
537605 if ( this . isStandalone ) {
538606 try {
@@ -706,6 +774,13 @@ export class CanvasEngine {
706774 ) ;
707775
708776 this . existingShapes . map ( ( shape : Shape ) => {
777+ const isBeingStreamed = [ ...this . remoteStreamingShapes . values ( ) ] . some (
778+ ( streamingShape ) => streamingShape . id === shape . id
779+ ) ;
780+
781+ if ( isBeingStreamed ) {
782+ return ;
783+ }
709784 if ( shape . type === "rectangle" ) {
710785 this . drawRect (
711786 shape . x ,
@@ -891,7 +966,7 @@ export class CanvasEngine {
891966 const screenX = x * this . scale + this . panX ;
892967 const screenY = y * this . scale + this . panY ;
893968
894- const cursorColor = getClientColor ( { userId, userName } ) ;
969+ const cursorColor : string = getClientColor ( { userId, userName } ) ;
895970 // const labelStrokeColor = this.currentTheme === "dark" ? "#2f6330" : COLOR_DRAG_CALL;
896971 const boxBackground = cursorColor ;
897972 const boxTextColor = COLOR_CHARCOAL_BLACK ;
@@ -951,13 +1026,12 @@ export class CanvasEngine {
9511026 this . ctx . fill ( ) ;
9521027 this . ctx . stroke ( ) ;
9531028
954- // Calculate label box positioning
9551029 const offsetX = screenX + pointerWidth / 2 ;
9561030 const offsetY = screenY + pointerHeight + 2 ;
9571031 const paddingX = 5 ;
9581032 const paddingY = 3 ;
9591033
960- this . ctx . font = "600 12px sans-serif" ;
1034+ this . ctx . font = "600 13px sans-serif" ;
9611035 const textMetrics = this . ctx . measureText ( userName ) ;
9621036 const textHeight =
9631037 textMetrics . actualBoundingBoxAscent +
@@ -976,7 +1050,7 @@ export class CanvasEngine {
9761050 this . ctx . strokeStyle = COLOR_WHITE ;
9771051 this . ctx . stroke ( ) ;
9781052
979- // Optional highlight stroke for speaker
1053+ // Optional highlight stroke for speaker // Option 2 for showing active indicator
9801054 // this.ctx.beginPath();
9811055 // this.ctx.roundRect(boxX - 2, boxY - 2, boxWidth + 4, boxHeight + 4, 8);
9821056 // this.ctx.strokeStyle = labelStrokeColor;
@@ -995,7 +1069,7 @@ export class CanvasEngine {
9951069
9961070 this . ctx . restore ( ) ;
9971071 } ) ;
998- // 🧼 Cleanup expired indicators (older than 1s)
1072+
9991073 this . remoteClickIndicators . forEach ( ( timestamp , key ) => {
10001074 if ( Date . now ( ) - timestamp > 1000 ) {
10011075 this . remoteClickIndicators . delete ( key ) ;
@@ -2264,7 +2338,6 @@ export class CanvasEngine {
22642338
22652339 this . ctx . restore ( ) ;
22662340 } else {
2267- // // For rough/sketchy style, use the existing implementation
22682341 const pathStr = points . reduce (
22692342 ( path , point , index ) =>
22702343 path +
@@ -2283,19 +2356,6 @@ export class CanvasEngine {
22832356 fillStyle
22842357 ) ;
22852358 this . roughCanvas . path ( pathStr , options ) ;
2286-
2287- // For rough/sketchy style, use the improved path with rough.js
2288- // const options = this.getRoughOptions(
2289- // strokeWidth,
2290- // strokeFill,
2291- // 0,
2292- // bgFill,
2293- // "solid",
2294- // fillStyle
2295- // );
2296-
2297- // // Use the SVG path data from perfect-freehand with rough.js
2298- // this.roughCanvas.path(svgPathData, options);
22992359 }
23002360 }
23012361
0 commit comments