@@ -93,6 +93,7 @@ type PatternType int
9393const (
9494 PatternStatic PatternType = iota
9595 PatternTyping
96+ PatternBackspace
9697)
9798
9899// FramePattern represents a detected pattern in frame sequences.
@@ -108,6 +109,10 @@ type FramePattern struct {
108109 StartCol int
109110 Text string
110111
112+ // For backspace patterns
113+ DeletedText string // Text that was deleted
114+ DeletedCount int // Number of characters deleted
115+
111116 // Store the initial and final states
112117 InitialState TerminalState
113118 FinalState TerminalState
@@ -538,6 +543,13 @@ func (g *SVGGenerator) detectPatterns() {
538543 continue
539544 }
540545
546+ // Try to detect backspace pattern
547+ if pattern , consumed := g .detectBackspacePattern (i ); pattern != nil {
548+ g .patterns = append (g .patterns , * pattern )
549+ i += consumed
550+ continue
551+ }
552+
541553 // If no pattern detected, treat as static frame
542554 frame := g .options .Frames [i ]
543555 g .patterns = append (g .patterns , FramePattern {
@@ -560,18 +572,26 @@ func (g *SVGGenerator) detectPatterns() {
560572 if g .options .Debug {
561573 typingPatterns := 0
562574 typingFrames := 0
575+ backspacePatterns := 0
576+ backspaceFrames := 0
563577 for _ , p := range g .patterns {
564- if p .Type == PatternTyping {
578+ switch p .Type {
579+ case PatternTyping :
565580 typingPatterns ++
566581 typingFrames += p .EndFrame - p .StartFrame + 1
582+ case PatternBackspace :
583+ backspacePatterns ++
584+ backspaceFrames += p .EndFrame - p .StartFrame + 1
567585 }
568586 }
569587 log .Printf ("Pattern detection analysis:" )
570588 log .Printf (" Total frames: %d" , len (g .options .Frames ))
571589 log .Printf (" Detected patterns: %d" , len (g .patterns ))
572- log .Printf (" Typing patterns: %d" , typingPatterns )
573- log .Printf (" Frames in typing patterns: %d (%.1f%%)" ,
574- typingFrames , float64 (typingFrames )/ float64 (len (g .options .Frames ))* 100 )
590+ log .Printf (" Typing patterns: %d (frames: %d)" , typingPatterns , typingFrames )
591+ log .Printf (" Backspace patterns: %d (frames: %d)" , backspacePatterns , backspaceFrames )
592+ totalOptimized := typingFrames + backspaceFrames
593+ log .Printf (" Total optimized frames: %d (%.1f%%)" ,
594+ totalOptimized , float64 (totalOptimized )/ float64 (len (g .options .Frames ))* 100 )
575595 }
576596}
577597
@@ -708,6 +728,126 @@ func (g *SVGGenerator) detectTypingPattern(start int) (*FramePattern, int) {
708728 return pattern , framesInPattern
709729}
710730
731+ // detectBackspacePattern looks for consecutive frames where text is being deleted.
732+ func (g * SVGGenerator ) detectBackspacePattern (start int ) (* FramePattern , int ) {
733+ if start >= len (g .options .Frames )- 1 {
734+ return nil , 0
735+ }
736+
737+ firstFrame := g .options .Frames [start ]
738+ line := firstFrame .CursorY
739+
740+ // Track the backspace sequence
741+ end := start + 1
742+ totalDeleted := 0
743+
744+ for end < len (g .options .Frames ) {
745+ prev := g .options .Frames [end - 1 ]
746+ curr := g .options .Frames [end ]
747+
748+ // Check if still on the same line
749+ if curr .CursorY != line {
750+ break
751+ }
752+
753+ // Check that only the cursor line changed
754+ if ! g .isOnlyLineChanged (prev , curr , line ) {
755+ break
756+ }
757+
758+ // Check if text is getting shorter (backspace pattern)
759+ if line < len (prev .Lines ) && line < len (curr .Lines ) {
760+ prevLine := prev .Lines [line ]
761+ currLine := curr .Lines [line ]
762+
763+ // For backspace, current line should be shorter
764+ if len (currLine ) >= len (prevLine ) {
765+ break
766+ }
767+
768+ // Check if it's a prefix (deleting from end)
769+ if ! strings .HasPrefix (prevLine , currLine ) {
770+ // Could be deletion in middle, but for now we'll break
771+ break
772+ }
773+
774+ // Track how many characters were deleted
775+ deleted := len (prevLine ) - len (currLine )
776+ totalDeleted += deleted
777+
778+ // Don't group huge deletions (likely line clear, not backspace)
779+ if deleted > 10 {
780+ break
781+ }
782+ } else {
783+ break
784+ }
785+
786+ end ++
787+ }
788+
789+ // Need at least 2 frames to consider it a backspace pattern
790+ framesInPattern := end - start
791+ if framesInPattern < 2 {
792+ return nil , 0
793+ }
794+
795+ // Need to have deleted at least 2 characters to be worth optimizing
796+ if totalDeleted < 2 {
797+ return nil , 0
798+ }
799+
800+ // Extract what was deleted
801+ lastFrame := g .options .Frames [end - 1 ]
802+ var deletedText string
803+
804+ if line < len (firstFrame .Lines ) && line < len (lastFrame .Lines ) {
805+ startLine := firstFrame .Lines [line ]
806+ endLine := lastFrame .Lines [line ]
807+
808+ if strings .HasPrefix (startLine , endLine ) {
809+ deletedText = startLine [len (endLine ):]
810+ }
811+ }
812+
813+ // Create states
814+ initialState := TerminalState {
815+ Lines : firstFrame .Lines ,
816+ LineColors : firstFrame .LineColors ,
817+ CursorX : firstFrame .CursorX ,
818+ CursorY : firstFrame .CursorY ,
819+ CursorChar : firstFrame .CursorChar ,
820+ }
821+
822+ finalState := TerminalState {
823+ Lines : lastFrame .Lines ,
824+ LineColors : lastFrame .LineColors ,
825+ CursorX : lastFrame .CursorX ,
826+ CursorY : lastFrame .CursorY ,
827+ CursorChar : lastFrame .CursorChar ,
828+ }
829+
830+ pattern := & FramePattern {
831+ Type : PatternBackspace ,
832+ StartFrame : start ,
833+ EndFrame : end - 1 ,
834+ StartTime : firstFrame .Timestamp ,
835+ EndTime : lastFrame .Timestamp ,
836+ Line : line ,
837+ DeletedText : deletedText ,
838+ DeletedCount : totalDeleted ,
839+ InitialState : initialState ,
840+ FinalState : finalState ,
841+ }
842+
843+ if g .options .Debug {
844+ log .Printf ("Detected backspace pattern: frames %d-%d, line %d, deleted: %q (saved %d frames)" ,
845+ start , end - 1 , line , deletedText , framesInPattern - 1 )
846+ }
847+
848+ return pattern , framesInPattern
849+ }
850+
711851// isOnlyLineChanged checks if only the specified line changed between frames.
712852func (g * SVGGenerator ) isOnlyLineChanged (prev , curr SVGFrame , targetLine int ) bool {
713853 // Check if number of lines changed significantly
@@ -776,6 +916,41 @@ func (g *SVGGenerator) generateTypingCSS(sb *strings.Builder, index int, pattern
776916 g .writeNewline (sb )
777917}
778918
919+ // generateBackspaceCSS generates CSS animation for a backspace pattern.
920+ func (g * SVGGenerator ) generateBackspaceCSS (sb * strings.Builder , index int , pattern FramePattern ) {
921+ // Calculate the width of the deleted text
922+ startWidth := float64 (len (pattern .DeletedText )) * g .charWidth
923+ duration := pattern .EndTime - pattern .StartTime
924+
925+ // Generate the keyframe animation (reverse of typing)
926+ sb .WriteString (fmt .Sprintf ("@keyframes backspace_%d {" , index ))
927+ g .writeNewline (sb )
928+ sb .WriteString (fmt .Sprintf (" from { width: %spx; }" , formatCoord (startWidth )))
929+ g .writeNewline (sb )
930+ sb .WriteString (" to { width: 0; }" )
931+ g .writeNewline (sb )
932+ sb .WriteString ("}" )
933+ g .writeNewline (sb )
934+
935+ // Generate the class for this backspace animation
936+ sb .WriteString (fmt .Sprintf (".backspace_%d {" , index ))
937+ g .writeNewline (sb )
938+ sb .WriteString (" overflow: hidden;" )
939+ g .writeNewline (sb )
940+ sb .WriteString (" white-space: nowrap;" )
941+ g .writeNewline (sb )
942+ sb .WriteString (" display: inline-block;" )
943+ g .writeNewline (sb )
944+ sb .WriteString (fmt .Sprintf (" animation: backspace_%d %ss steps(%d, end) forwards;" ,
945+ index , formatDuration (duration ), pattern .DeletedCount ))
946+ g .writeNewline (sb )
947+ sb .WriteString (fmt .Sprintf (" animation-delay: %ss;" , formatDuration (pattern .StartTime )))
948+ g .writeNewline (sb )
949+ sb .WriteString ("}" )
950+ g .writeNewline (sb )
951+ g .writeNewline (sb )
952+ }
953+
779954// generateStyles creates the CSS styles and animations.
780955func (g * SVGGenerator ) generateStyles () string {
781956 var sb strings.Builder
@@ -785,8 +960,11 @@ func (g *SVGGenerator) generateStyles() string {
785960
786961 // Generate typing animations for detected patterns
787962 for i , pattern := range g .patterns {
788- if pattern .Type == PatternTyping {
963+ switch pattern .Type {
964+ case PatternTyping :
789965 g .generateTypingCSS (& sb , i , pattern )
966+ case PatternBackspace :
967+ g .generateBackspaceCSS (& sb , i , pattern )
790968 }
791969 }
792970
0 commit comments