@@ -164,6 +164,8 @@ function Canvas({
164164 const lastDrawnStateRef = useRef ( null ) ; // Track last drawn state to avoid redundant redraws
165165 const isDrawingInProgressRef = useRef ( false ) ; // Prevent concurrent drawing operations
166166 const offscreenCanvasRef = useRef ( null ) ; // Offscreen canvas for flicker free rendering
167+ const cachedCanvasRef = useRef ( null ) ; // Cached canvas for incremental rendering
168+ const cachedDrawingIdsRef = useRef ( new Set ( ) ) ; // Track which drawings are in the cache
167169 const forceNextRedrawRef = useRef ( false ) ; // Force next redraw even if signature matches for undo redo
168170 const [ historyMode , setHistoryMode ] = useState ( false ) ;
169171 const [ historyRange , setHistoryRange ] = useState ( null ) ; // {start, end} in epoch ms
@@ -888,6 +890,8 @@ function Canvas({
888890 drawingType : "stamp" ,
889891 stampData : stamp ,
890892 stampSettings : settings ,
893+ isPending : true , // Mark as pending for visual confirmation
894+ opacity : 0.5 , // Reduced opacity for pending stamps
891895 }
892896 ) ;
893897
@@ -919,6 +923,16 @@ function Canvas({
919923
920924 console . log ( "Stamp submitted successfully:" , stampDrawing . drawingId ) ;
921925
926+ // Mark stamp as confirmed
927+ stampDrawing . isPending = false ;
928+ stampDrawing . opacity = 1.0 ;
929+ confirmedStrokesRef . current . add ( stampDrawing . drawingId ) ;
930+
931+ // Trigger a redraw to show the confirmed state
932+ requestAnimationFrame ( ( ) => {
933+ drawAllDrawings ( ) ;
934+ } ) ;
935+
922936 if ( currentRoomId ) {
923937 checkUndoRedoAvailability (
924938 auth ,
@@ -1712,6 +1726,38 @@ function Canvas({
17121726 return ;
17131727 }
17141728
1729+ // Check if we can do incremental rendering (only new drawings added, no cuts/filters/etc)
1730+ const canUseIncrementalRendering = lastDrawnStateRef . current &&
1731+ cachedCanvasRef . current &&
1732+ cachedDrawingIdsRef . current . size > 0 &&
1733+ combined . length > cachedDrawingIdsRef . current . size &&
1734+ ! combined . some ( d => d . drawingType === "filter" || ( d . pathData && d . pathData . tool === "cut" ) ) &&
1735+ currentTemplateObjects ?. length === 0 ;
1736+
1737+ let newDrawingsOnly = [ ] ;
1738+ if ( canUseIncrementalRendering ) {
1739+ // Find drawings that aren't in the cache
1740+ newDrawingsOnly = combined . filter ( d => ! cachedDrawingIdsRef . current . has ( d . drawingId ) ) ;
1741+
1742+ // Verify all cached drawings are still present
1743+ const currentIds = new Set ( combined . map ( d => d . drawingId ) ) ;
1744+ const allCachedPresent = Array . from ( cachedDrawingIdsRef . current ) . every ( id => currentIds . has ( id ) ) ;
1745+
1746+ if ( newDrawingsOnly . length > 0 && allCachedPresent && newDrawingsOnly . length <= 5 ) {
1747+ console . log ( `Incremental rendering: adding ${ newDrawingsOnly . length } new drawings` ) ;
1748+ // We can use incremental rendering!
1749+ } else {
1750+ // Fall back to full redraw
1751+ newDrawingsOnly = [ ] ;
1752+ cachedCanvasRef . current = null ;
1753+ cachedDrawingIdsRef . current . clear ( ) ;
1754+ }
1755+ } else {
1756+ // Clear cache - we need full redraw
1757+ cachedCanvasRef . current = null ;
1758+ cachedDrawingIdsRef . current . clear ( ) ;
1759+ }
1760+
17151761 // Clear force flag after checking it
17161762 forceNextRedrawRef . current = false ;
17171763 lastDrawnStateRef . current = stateSignature ;
@@ -1728,7 +1774,16 @@ function Canvas({
17281774
17291775 const offscreenContext = offscreenCanvasRef . current . getContext ( "2d" ) ;
17301776 offscreenContext . imageSmoothingEnabled = false ;
1731- offscreenContext . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
1777+
1778+ // If we can do incremental rendering, start from cached canvas
1779+ if ( newDrawingsOnly . length > 0 && cachedCanvasRef . current ) {
1780+ console . log ( "[drawAllDrawings] Using incremental rendering - copying from cache" ) ;
1781+ offscreenContext . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
1782+ offscreenContext . drawImage ( cachedCanvasRef . current , 0 , 0 ) ;
1783+ } else {
1784+ // Full redraw
1785+ offscreenContext . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
1786+ }
17321787
17331788 // This avoids async rendering issues with image stamps
17341789 const stampsToRender = [ ] ;
@@ -1881,7 +1936,11 @@ function Canvas({
18811936 const maskedOriginals = new Set ( ) ;
18821937 let seenAnyCut = false ;
18831938
1884- for ( const drawing of regularDrawings ) {
1939+ // If doing incremental rendering, only draw new drawings
1940+ const drawingsToRender = newDrawingsOnly . length > 0 ? newDrawingsOnly : regularDrawings ;
1941+ console . log ( `[drawAllDrawings] Rendering ${ drawingsToRender . length } drawings (incremental: ${ newDrawingsOnly . length > 0 } )` ) ;
1942+
1943+ for ( const drawing of drawingsToRender ) {
18851944 // If this is a cut record, apply the erase to the canvas now.
18861945 if ( drawing && drawing . pathData && drawing . pathData . tool === "cut" ) {
18871946 seenAnyCut = true ;
@@ -1976,6 +2035,13 @@ function Canvas({
19762035 offscreenContext . globalAlpha = 0.1 ;
19772036 }
19782037 }
2038+
2039+ // Apply opacity for pending strokes (visual confirmation)
2040+ if ( drawing . isPending ) {
2041+ offscreenContext . globalAlpha *= 0.5 ;
2042+ } else if ( drawing . opacity !== undefined && drawing . opacity !== 1.0 ) {
2043+ offscreenContext . globalAlpha *= drawing . opacity ;
2044+ }
19792045
19802046 // Stamps have pathData as array but need special rendering - render inline to preserve z-order
19812047 if ( drawing . drawingType === "stamp" && drawing . stampData && drawing . stampSettings && Array . isArray ( drawing . pathData ) && drawing . pathData . length > 0 ) {
@@ -2228,10 +2294,27 @@ function Canvas({
22282294 }
22292295
22302296 // Copy offscreen canvas to visible canvas atomically
2231- console . log ( "[drawAllDrawings] Copying offscreen canvas to visible canvas. Total strokes rendered:" , regularDrawings . length , "filters:" , filterDrawings . length ) ;
2297+ console . log ( "[drawAllDrawings] Copying offscreen canvas to visible canvas. Total strokes rendered:" , drawingsToRender . length , "filters:" , filterDrawings . length ) ;
22322298 context . imageSmoothingEnabled = false ;
22332299 context . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
22342300 context . drawImage ( offscreenCanvasRef . current , 0 , 0 ) ;
2301+
2302+ // Update cache after successful render (only if no filters/cuts and not in incremental mode)
2303+ if ( filterDrawings . length === 0 && ! combined . some ( d => d . pathData && d . pathData . tool === "cut" ) ) {
2304+ if ( ! cachedCanvasRef . current || cachedCanvasRef . current . width !== canvasWidth || cachedCanvasRef . current . height !== canvasHeight ) {
2305+ cachedCanvasRef . current = document . createElement ( "canvas" ) ;
2306+ cachedCanvasRef . current . width = canvasWidth ;
2307+ cachedCanvasRef . current . height = canvasHeight ;
2308+ }
2309+ const cacheContext = cachedCanvasRef . current . getContext ( "2d" ) ;
2310+ cacheContext . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
2311+ cacheContext . drawImage ( offscreenCanvasRef . current , 0 , 0 ) ;
2312+
2313+ // Update cached drawing IDs
2314+ cachedDrawingIdsRef . current = new Set ( combined . map ( d => d . drawingId ) ) ;
2315+ console . log ( `[drawAllDrawings] Cached ${ cachedDrawingIdsRef . current . size } drawings for future incremental rendering` ) ;
2316+ }
2317+
22352318 console . log ( "[drawAllDrawings] Canvas update complete" ) ;
22362319 } catch ( e ) {
22372320 console . error ( "Error in drawAllDrawings:" , e ) ;
@@ -3384,6 +3467,8 @@ function Canvas({
33843467 brushType : currentBrushType ,
33853468 brushParams : brushParams ,
33863469 drawingType : "stroke" ,
3470+ isPending : true , // Mark as pending for visual confirmation
3471+ opacity : 0.5 , // Reduced opacity for pending strokes
33873472 }
33883473 ) ;
33893474 newDrawing . roomId = currentRoomId ;
@@ -3422,7 +3507,15 @@ function Canvas({
34223507 setRedoAvailable
34233508 ) ;
34243509
3425- // Don't remove from pending here - let mergedRefreshCanvas or socket confirmation handle it
3510+ // Mark stroke as confirmed by updating it in userData
3511+ newDrawing . isPending = false ;
3512+ newDrawing . opacity = 1.0 ;
3513+ confirmedStrokesRef . current . add ( newDrawing . drawingId ) ;
3514+
3515+ // Trigger a redraw to show the confirmed state
3516+ requestAnimationFrame ( ( ) => {
3517+ drawAllDrawings ( ) ;
3518+ } ) ;
34263519
34273520 if ( currentRoomId ) {
34283521 checkUndoRedoAvailability (
@@ -3526,6 +3619,8 @@ function Canvas({
35263619 brushType : currentBrushType ,
35273620 brushParams : brushParams ,
35283621 drawingType : "shape" ,
3622+ isPending : true , // Mark as pending for visual confirmation
3623+ opacity : 0.5 , // Reduced opacity for pending strokes
35293624 }
35303625 ) ;
35313626 newDrawing . roomId = currentRoomId ;
@@ -3555,7 +3650,15 @@ function Canvas({
35553650 setRedoAvailable
35563651 ) ;
35573652
3558- // Don't remove from pending here - let mergedRefreshCanvas or socket confirmation handle it
3653+ // Mark stroke as confirmed
3654+ newDrawing . isPending = false ;
3655+ newDrawing . opacity = 1.0 ;
3656+ confirmedStrokesRef . current . add ( newDrawing . drawingId ) ;
3657+
3658+ // Trigger a redraw to show the confirmed state
3659+ requestAnimationFrame ( ( ) => {
3660+ drawAllDrawings ( ) ;
3661+ } ) ;
35593662
35603663 // Update undo/redo availability after shape submission
35613664 if ( currentRoomId ) {
0 commit comments