diff --git a/banner.go b/banner.go index f73c56c..43af004 100644 --- a/banner.go +++ b/banner.go @@ -40,7 +40,6 @@ func NewTextData(text string, size float64, multiline bool) *TextData { return &TextData{splits, size, false, -1, -1, -1, -1} } -// Consistent outline thickness based on font size func getOutlineThickness(size float64) int { thickness := int(size / 8) if thickness < 1 { @@ -51,7 +50,6 @@ func getOutlineThickness(size float64) int { return thickness } -// Actual bounding box for text including outline func getTextBounds(text []string, size float64) (width, height int) { face, err := createFontFace(size) if err != nil { @@ -146,8 +144,7 @@ func drawTextWithOutline(canvas *image.RGBA, text []string, x, y int, textColor, } } -// Place all elements to canvas without overlaps -func findTextPositions(textData []*TextData) { +func findTextPositions(textData []*TextData, filename string) { padding := 10 for i, data := range textData { @@ -182,7 +179,7 @@ func findTextPositions(textData []*TextData) { y := rand.IntN(maxY-minY+1) + minY // bounding box for cur. text - currentRect := struct{ x, y, w, h int }{ + curRect := struct{ x, y, w, h int }{ x: x, y: y, w: w, @@ -204,7 +201,7 @@ func findTextPositions(textData []*TextData) { h: other.H + padding, } - if rectsOverlap(currentRect, otherRect) { + if rectsOverlap(curRect, otherRect) { overlaps = true break } @@ -219,35 +216,84 @@ func findTextPositions(textData []*TextData) { } } - // fallback: use grid positioning + // fallback: place to a free corner if !placed { - log.Warnf("Using fallback position for text '%v'", data.Text) + log.Debugf("Using fallback position for text '%s' in '%s'", data.Text, filename) - gridCols := 2 - col := i % gridCols - row := i / gridCols - - cellWidth := config.BannerWidth / gridCols - cellHeight := config.BannerHeight / 3 // NOTE: assuming max 3 rows - - data.X = col*cellWidth + (cellWidth-w)/2 - data.Y = row*cellHeight + (cellHeight-h)/2 - - // ensure within bounds - if data.X < padding { - data.X = padding - } - if data.Y < padding { - data.Y = padding - } - if data.X+w > config.BannerWidth-padding { - data.X = config.BannerWidth - w - padding - } - if data.Y+h > config.BannerHeight-padding { - data.Y = config.BannerHeight - h - padding + corners := []struct { + name string + x, y int + }{ + {"top-left", padding, padding}, + {"top-right", config.BannerWidth - w - padding, padding}, + {"bottom-left", padding, config.BannerHeight - h - padding}, + {"bottom-right", config.BannerWidth - w - padding, config.BannerHeight - h - padding}, } + bestCorner := 0 + minOverlapArea := int(^uint(0) >> 1) // max. int + + // try each corner and find the one with least overlap + for cornerIdx, corner := range corners { + curRect := struct{ x, y, w, h int }{ + x: corner.x, + y: corner.y, + w: w, + h: h, + } + + totalOverlapArea := 0 + + // calculate total overlap area with existing texts + for j := 0; j < i; j++ { + other := textData[j] + if !other.Positioned { + continue + } + + otherRect := struct{ x, y, w, h int }{ + x: other.X, + y: other.Y, + w: other.W, + h: other.H, + } + + if rectsOverlap(curRect, otherRect) { + // intersection rectangle + overlapLeft := max(curRect.x, otherRect.x) + overlapTop := max(curRect.y, otherRect.y) + overlapRight := min(curRect.x+curRect.w, otherRect.x+otherRect.w) + overlapBottom := min(curRect.y+curRect.h, otherRect.y+otherRect.h) + + overlapWidth := overlapRight - overlapLeft + overlapHeight := overlapBottom - overlapTop + overlapArea := overlapWidth * overlapHeight + + totalOverlapArea += overlapArea + } + + if totalOverlapArea < minOverlapArea { + minOverlapArea = totalOverlapArea + bestCorner = cornerIdx + } + + // if no overlap, use the pos. immediately + if totalOverlapArea == 0 { + break + } + } + } + + // place at the best corner + data.X = corners[bestCorner].x + data.Y = corners[bestCorner].y data.Positioned = true + + if minOverlapArea > 0 { + log.Debugf("Placed text '%v' at %s corner with some overlap (area: %d)", data.Text, corners[bestCorner].name, minOverlapArea) + } else { + log.Debugf("Placed text '%v' at %s corner with no overlap", data.Text, corners[bestCorner].name) + } } } } @@ -363,9 +409,10 @@ func genPerlinBG() *image.RGBA { } func genBanner(textData []*TextData) error { + fn := fmt.Sprintf("banner_%d.png", time.Now().UnixMicro()) canvas := genPerlinBG() - findTextPositions(textData) + findTextPositions(textData, fn) for _, td := range textData { if td.Positioned { @@ -374,7 +421,6 @@ func genBanner(textData []*TextData) error { } } - fn := fmt.Sprintf("banner_%d.png", time.Now().UnixMicro()) fp := filepath.Join(bannersDir, fn) file, err := os.Create(fp) if err != nil {