@@ -54,6 +54,7 @@ type WebSocketConnection = {
54
54
connected : boolean ;
55
55
} ;
56
56
57
+ // NOTE: Comments in this Canvas Engine are not AI generated. This are for my personal understanding.
57
58
export class CanvasEngine {
58
59
private canvas : HTMLCanvasElement ;
59
60
private ctx : CanvasRenderingContext2D ;
@@ -126,6 +127,7 @@ export class CanvasEngine {
126
127
private remoteClickIndicators : Map < string , number > = new Map ( ) ;
127
128
128
129
private currentTheme : "light" | "dark" | null = null ;
130
+ private onLiveUpdateFromSelection ?: ( shape : Shape ) => void ;
129
131
130
132
constructor (
131
133
canvas : HTMLCanvasElement ,
@@ -176,6 +178,12 @@ export class CanvasEngine {
176
178
) ;
177
179
}
178
180
} ) ;
181
+ this . SelectionController . setOnLiveUpdate ( ( shape ) => {
182
+ this . streamShapeUpdate ( shape ) ;
183
+ } ) ;
184
+ this . SelectionController . setOnDragOrResizeCursorMove ( ( x , y ) => {
185
+ this . sendCursorMove ( x , y ) ;
186
+ } ) ;
179
187
if ( ! this . isStandalone && this . token && this . roomId ) {
180
188
console . log ( "✅Connecting to WebSocket…" ) ;
181
189
this . connectWebSocket ( ) ;
@@ -332,11 +340,6 @@ export class CanvasEngine {
332
340
this . remoteStreamingShapes . set ( streamKey , streamedShape ) ;
333
341
const userConnKey = `${ data . userId } -${ data . connectionId } ` ;
334
342
this . remoteClickIndicators . set ( userConnKey , Date . now ( ) ) ;
335
- // console.log(
336
- // "this.remoteClickIndicators = ",
337
- // this.remoteClickIndicators
338
- // );
339
-
340
343
this . clearCanvas ( ) ;
341
344
} catch ( err ) {
342
345
console . error ( "Error handling streamed shape:" , err ) ;
@@ -378,6 +381,29 @@ export class CanvasEngine {
378
381
}
379
382
break ;
380
383
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
+
381
407
case WsDataType . DRAW :
382
408
case WsDataType . UPDATE :
383
409
if (
@@ -533,6 +559,48 @@ export class CanvasEngine {
533
559
} , this . streamingUpdateInterval ) ;
534
560
}
535
561
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
+
536
604
async init ( ) {
537
605
if ( this . isStandalone ) {
538
606
try {
@@ -706,6 +774,13 @@ export class CanvasEngine {
706
774
) ;
707
775
708
776
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
+ }
709
784
if ( shape . type === "rectangle" ) {
710
785
this . drawRect (
711
786
shape . x ,
@@ -891,7 +966,7 @@ export class CanvasEngine {
891
966
const screenX = x * this . scale + this . panX ;
892
967
const screenY = y * this . scale + this . panY ;
893
968
894
- const cursorColor = getClientColor ( { userId, userName } ) ;
969
+ const cursorColor : string = getClientColor ( { userId, userName } ) ;
895
970
// const labelStrokeColor = this.currentTheme === "dark" ? "#2f6330" : COLOR_DRAG_CALL;
896
971
const boxBackground = cursorColor ;
897
972
const boxTextColor = COLOR_CHARCOAL_BLACK ;
@@ -951,13 +1026,12 @@ export class CanvasEngine {
951
1026
this . ctx . fill ( ) ;
952
1027
this . ctx . stroke ( ) ;
953
1028
954
- // Calculate label box positioning
955
1029
const offsetX = screenX + pointerWidth / 2 ;
956
1030
const offsetY = screenY + pointerHeight + 2 ;
957
1031
const paddingX = 5 ;
958
1032
const paddingY = 3 ;
959
1033
960
- this . ctx . font = "600 12px sans-serif" ;
1034
+ this . ctx . font = "600 13px sans-serif" ;
961
1035
const textMetrics = this . ctx . measureText ( userName ) ;
962
1036
const textHeight =
963
1037
textMetrics . actualBoundingBoxAscent +
@@ -976,7 +1050,7 @@ export class CanvasEngine {
976
1050
this . ctx . strokeStyle = COLOR_WHITE ;
977
1051
this . ctx . stroke ( ) ;
978
1052
979
- // Optional highlight stroke for speaker
1053
+ // Optional highlight stroke for speaker // Option 2 for showing active indicator
980
1054
// this.ctx.beginPath();
981
1055
// this.ctx.roundRect(boxX - 2, boxY - 2, boxWidth + 4, boxHeight + 4, 8);
982
1056
// this.ctx.strokeStyle = labelStrokeColor;
@@ -995,7 +1069,7 @@ export class CanvasEngine {
995
1069
996
1070
this . ctx . restore ( ) ;
997
1071
} ) ;
998
- // 🧼 Cleanup expired indicators (older than 1s)
1072
+
999
1073
this . remoteClickIndicators . forEach ( ( timestamp , key ) => {
1000
1074
if ( Date . now ( ) - timestamp > 1000 ) {
1001
1075
this . remoteClickIndicators . delete ( key ) ;
@@ -2264,7 +2338,6 @@ export class CanvasEngine {
2264
2338
2265
2339
this . ctx . restore ( ) ;
2266
2340
} else {
2267
- // // For rough/sketchy style, use the existing implementation
2268
2341
const pathStr = points . reduce (
2269
2342
( path , point , index ) =>
2270
2343
path +
@@ -2283,19 +2356,6 @@ export class CanvasEngine {
2283
2356
fillStyle
2284
2357
) ;
2285
2358
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);
2299
2359
}
2300
2360
}
2301
2361
0 commit comments