@@ -5,22 +5,6 @@ import (
55 "unicode/utf8"
66)
77
8- // runeAt is a helper to safely get a rune at a specific column index within a string.
9- // It handles multi-byte runes correctly.
10- func runeAt (s string , x int ) (rune , bool ) {
11- if x < 0 {
12- return ' ' , false
13- }
14- i := 0
15- for _ , r := range s {
16- if i == x {
17- return r , true
18- }
19- i ++
20- }
21- return ' ' , false
22- }
23-
248// maxRowLen is a helper to find the maximum rune length among all rows of a glyph,
259// effectively determining its bounding box width.
2610func maxRowLen (rows []string ) int {
@@ -50,140 +34,100 @@ func normalizeGlyph(glyph []string, height int) []string {
5034 out [i ] = row
5135 } else {
5236 // Pad with spaces (not empty strings) to maintain consistent width
53- out [i ] = strings .Repeat (" " , maxRowLen ( glyph ) )
37+ out [i ] = strings .Repeat (" " , maxLen )
5438 }
5539 }
5640 return out
5741}
5842
59- // getActivePixels extracts all non-space pixel coordinates for a given glyph.
60- // These are the "ink" pixels that contribute to the character's visual form.
61- func getActivePixels (glyph []string ) map [pixelCoord ]bool {
62- activePixels := make (map [pixelCoord ]bool )
63- for y , row := range glyph {
64- for x := range utf8 .RuneCountInString (row ) {
65- r , _ := runeAt (row , x )
66- // ' ' (space) and '\u0000' (null character) are considered empty/background.
67- if r != ' ' && r != 0 {
68- activePixels [pixelCoord {float64 (x ), y , false }] = true
69- }
70- }
43+ // computeKerning calculates the horizontal offset needed to align glyphB relative to glyphA
44+ // such that the minimum distance between their visible pixels is exactly 1 (touching).
45+ // This allows the renderer to add precise character spacing on top of this baseline.
46+ func computeKerning (glyphA , glyphB []string ) int {
47+ if len (glyphA ) == 0 || len (glyphB ) == 0 {
48+ return 0
7149 }
72- return activePixels
73- }
7450
75- // Helper function to calculate absolute value for float64
76- func abs (x float64 ) float64 {
77- if x < 0 {
78- return - x
79- }
80- return x
81- }
51+ // Normalize heights for line-by-line comparison
52+ h := max (len (glyphA ), len (glyphB ))
53+ a := normalizeGlyph (glyphA , h )
54+ b := normalizeGlyph (glyphB , h )
8255
83- // checkCollisionWithSmartBuffer uses enhanced visual analysis to allow better kerning
84- func checkCollisionWithSmartBuffer (glyphA , glyphB []string , widthA int , spacing int ) bool {
85- aPixels := getActivePixels (glyphA )
56+ widthA := maxRowLen (a )
57+ if widthA == 0 {
58+ return 0
59+ }
8660
87- // Check for half-pixel alignment issues
88- heightDiff := len (glyphA ) - len (glyphB )
89- needsHalfPixelAdjustment := heightDiff % 2 != 0
61+ minDist := 1000 // effectively infinity
62+ hasOverlap := false
9063
91- for yB , rowB := range glyphB {
92- for xB := 0 ; xB < utf8 .RuneCountInString (rowB ); xB ++ {
93- rB , _ := runeAt (rowB , xB )
94- if rB == ' ' || rB == 0 {
95- continue
96- }
64+ // Global bounds for fallback (handling non-vertically overlapping characters)
65+ maxAGlobal := - 1
66+ minBGlobal := 1000
9767
98- // Calculate the absolute X position of glyphB's pixel
99- xAbsB := float64 ( widthA + spacing + xB )
100- yAbsB := yB
68+ for y := 0 ; y < h ; y ++ {
69+ rowA := [] rune ( a [ y ] )
70+ rowB := [] rune ( b [ y ])
10171
102- // Apply half-pixel adjustment if needed
103- if needsHalfPixelAdjustment && yAbsB >= len (glyphB )/ 2 {
104- xAbsB += 0.5 // Adjust by half a pixel for the bottom half
72+ // Find rightmost pixel in A on this line
73+ maxA := - 1
74+ for x := len (rowA ) - 1 ; x >= 0 ; x -- {
75+ if rowA [x ] != ' ' && rowA [x ] != 0 {
76+ maxA = x
77+ break
10578 }
79+ }
10680
107- // Check for direct overlap
108- for coord := range aPixels {
109- // Convert to float64 for comparison
110- coordX := coord .x
111- coordY := coord .y
112-
113- // Check if this pixel would overlap with any pixel in glyphA
114- if abs (coordX - xAbsB ) < 1.0 && coordY == yAbsB {
115- return true
116- }
81+ // Find leftmost pixel in B on this line
82+ minB := - 1
83+ for x := 0 ; x < len (rowB ); x ++ {
84+ if rowB [x ] != ' ' && rowB [x ] != 0 {
85+ minB = x
86+ break
11787 }
88+ }
11889
119- // Enhanced adjacency check
120- conflictCount := 0
121- for dy := - 1 ; dy <= 1 ; dy ++ {
122- for dx := - 1 ; dx <= 1 ; dx ++ {
123- if dx == 0 && dy == 0 {
124- continue
125- }
126-
127- checkX := xAbsB + float64 (dx )
128- checkY := yAbsB + dy
129-
130- for coord := range aPixels {
131- coordX := coord .x
132- coordY := coord .y
133-
134- // Check proximity with half-pixel precision
135- xDist := abs (coordX - checkX )
136- yDist := abs (float64 (coordY ) - float64 (checkY ))
137-
138- if xDist < 1.0 && yDist < 1.0 {
139- conflictCount ++
140- // Weight horizontal conflicts more heavily
141- if dx != 0 && dy == 0 {
142- conflictCount += 2
143- }
144- }
145- }
146- }
90+ // Update global bounds
91+ if maxA != - 1 {
92+ if maxA > maxAGlobal {
93+ maxAGlobal = maxA
14794 }
148-
149- // Only consider it a collision if there's significant conflict
150- if conflictCount >= 3 {
151- return true
95+ }
96+ if minB != - 1 {
97+ if minB < minBGlobal {
98+ minBGlobal = minB
15299 }
153100 }
154- }
155-
156- return false
157- }
158-
159- // computeKerning calculates the minimum horizontal spacing between two characters (glyphA and glyphB)
160- // such that their active pixels do not touch (even by corners). This spacing is constrained to -1, 0, or +1.
161- func computeKerning (glyphA , glyphB []string ) int {
162- // If either glyph has no visual width (e.g., empty bitmap), return neutral spacing
163- if maxRowLen (glyphA ) == 0 || maxRowLen (glyphB ) == 0 {
164- return 0 // No adjustment needed for empty glyphs
165- }
166-
167- hA , hB := len (glyphA ), len (glyphB )
168- h := max (hA , hB )
169101
170- a := normalizeGlyph (glyphA , h )
171- b := normalizeGlyph (glyphB , h )
172- widthA := maxRowLen (a )
173-
174- // Check spacing options in order: -1 (tight), 0 (normal), +1 (extra space)
175- // Return the minimum safe spacing within our constraint range
176-
177- // First try -1 (tight kerning/tucking)
178- if ! checkCollisionWithSmartBuffer (a , b , widthA , - 1 ) {
179- return - 1 // Safe to tuck closer
102+ // If both have pixels on this line, calculate the horizontal distance
103+ if maxA != - 1 && minB != - 1 {
104+ // Calculate distance: (Start of B - Start of A)
105+ // We imagine B starts at WidthA.
106+ // PosA = maxA
107+ // PosB = WidthA + minB
108+ // Distance = PosB - PosA
109+ dist := (widthA + minB ) - maxA
110+ if dist < minDist {
111+ minDist = dist
112+ }
113+ hasOverlap = true
114+ }
180115 }
181116
182- // Then try 0 (normal spacing)
183- if ! checkCollisionWithSmartBuffer (a , b , widthA , 0 ) {
184- return 0 // Normal spacing works
117+ // If no lines overlap (e.g., punctuation marks at different heights like ' and .),
118+ // fall back to the bounding box horizontal distance.
119+ if ! hasOverlap {
120+ if maxAGlobal != - 1 && minBGlobal != 1000 {
121+ minDist = (widthA + minBGlobal ) - maxAGlobal
122+ } else {
123+ // One or both glyphs are empty space
124+ return 0
125+ }
185126 }
186127
187- // Finally, use +1 (extra space needed)
188- return 1 // Need extra space to prevent collision
128+ // We want the closest pixels to be adjacent (distance 1 pixel).
129+ // This creates a visual gap of 0.
130+ // The renderer will add the user's specific spacing on top of this.
131+ // Adjustment = DesiredDistance(1) - ActualDistance(minDist)
132+ return 1 - minDist
189133}
0 commit comments