Added a new font and increased the size and resolution of the result

image
This commit is contained in:
2026-05-01 18:40:57 -06:00
parent 9ea2cf8c34
commit 4bb5ece677
2 changed files with 108 additions and 101 deletions
Binary file not shown.
+108 -101
View File
@@ -1,6 +1,7 @@
package reports package reports
import ( import (
_ "embed"
"fmt" "fmt"
"image/color" "image/color"
"image/jpeg" "image/jpeg"
@@ -15,14 +16,29 @@ import (
"github.com/fogleman/gg" "github.com/fogleman/gg"
) )
//go:embed BitCount.ttf
var fontData []byte
var fontPath string
func init() {
tmpDir := os.TempDir()
fontPath = filepath.Join(tmpDir, "BitCount.ttf")
if _, err := os.Stat(fontPath); os.IsNotExist(err) {
if err := os.WriteFile(fontPath, fontData, 0644); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to write embedded font: %v\n", err)
}
}
}
const ( const (
canvasW = 1400 canvasW = 2100
canvasH = 1160 canvasH = 1740
gridGap = 20 gridGap = 30
radius = 24 radius = 36
unitCol = 325 unitCol = 487
twoCol = 670 twoCol = 1005
fourCol = 1360 fourCol = 2040
) )
var ( var (
@@ -30,16 +46,16 @@ var (
cardBg = color.RGBA{24, 24, 27, 255} cardBg = color.RGBA{24, 24, 27, 255}
cardBgPurple = color.RGBA{30, 20, 36, 255} cardBgPurple = color.RGBA{30, 20, 36, 255}
cardBgBlue = color.RGBA{15, 23, 42, 255} cardBgBlue = color.RGBA{15, 23, 42, 255}
accentMagenta = color.RGBA{192, 132, 252, 255} accentMagenta = color.RGBA{220, 90, 255, 255}
accentCyan = color.RGBA{45, 212, 191, 255} accentCyan = color.RGBA{0, 244, 255, 255}
accentOrange = color.RGBA{251, 146, 60, 255} accentOrange = color.RGBA{255, 150, 0, 255}
accentGreen = color.RGBA{74, 222, 128, 255} accentGreen = color.RGBA{34, 255, 100, 255}
accentBlue = color.RGBA{96, 165, 250, 255} accentBlue = color.RGBA{100, 180, 255, 255}
textPrimary = color.RGBA{244, 244, 245, 255} textPrimary = color.RGBA{255, 255, 255, 255}
textSecondary = color.RGBA{161, 161, 170, 255} textSecondary = color.RGBA{200, 200, 215, 255}
textMuted = color.RGBA{113, 113, 122, 255} textMuted = color.RGBA{150, 150, 170, 255}
borderColor = color.RGBA{39, 39, 42, 255} borderColor = color.RGBA{60, 60, 70, 255}
barTrack = color.RGBA{39, 39, 42, 255} barTrack = color.RGBA{50, 50, 65, 255}
) )
type bentoCard struct { type bentoCard struct {
@@ -54,13 +70,13 @@ func col(n int) float64 {
func cardLayout() []bentoCard { func cardLayout() []bentoCard {
r1y := float64(gridGap) r1y := float64(gridGap)
r1h := float64(240) r1h := float64(360)
r2y := r1y + r1h + gridGap r2y := r1y + r1h + gridGap
r2h := float64(240) r2h := float64(360)
r3y := r2y + r2h + gridGap r3y := r2y + r2h + gridGap
r3h := float64(300) r3h := float64(450)
r4y := r3y + r3h + gridGap r4y := r3y + r3h + gridGap
r4h := float64(280) r4h := float64(420)
return []bentoCard{ return []bentoCard{
{col(0), r1y, twoCol, r1h, cardBgPurple, accentMagenta}, {col(0), r1y, twoCol, r1h, cardBgPurple, accentMagenta},
@@ -75,17 +91,8 @@ func cardLayout() []bentoCard {
} }
func loadFont(dc *gg.Context, size float64) { func loadFont(dc *gg.Context, size float64) {
paths := []string{ if err := dc.LoadFontFace(fontPath, size); err != nil {
"/System/Library/Fonts/Helvetica.ttc", fmt.Fprintf(os.Stderr, "Warning: failed to load embedded font: %v\n", err)
"/System/Library/Fonts/SFNS.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
}
for _, p := range paths {
if err := dc.LoadFontFace(p, size); err == nil {
return
}
} }
} }
@@ -100,27 +107,27 @@ func drawCard(dc *gg.Context, c bentoCard) {
} }
func label(dc *gg.Context, c bentoCard, text string) { func label(dc *gg.Context, c bentoCard, text string) {
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString(text, c.x+24, c.y+32) dc.DrawString(text, c.x+36, c.y+48)
} }
func bigNum(dc *gg.Context, c bentoCard, value, sub string) { func bigNum(dc *gg.Context, c bentoCard, value, sub string) {
loadFont(dc, 64) loadFont(dc, 96)
dc.SetColor(c.accent) dc.SetColor(c.accent)
dc.DrawString(value, c.x+24, c.y+110) dc.DrawString(value, c.x+36, c.y+165)
if sub != "" { if sub != "" {
loadFont(dc, 14) loadFont(dc, 21)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString(sub, c.x+24, c.y+136) dc.DrawString(sub, c.x+36, c.y+204)
} }
} }
func bigText(dc *gg.Context, c bentoCard, value string, size float64) { func bigText(dc *gg.Context, c bentoCard, value string, size float64) {
loadFont(dc, size) loadFont(dc, size*1.5)
dc.SetColor(c.accent) dc.SetColor(c.accent)
dc.DrawString(value, c.x+24, c.y+110) dc.DrawString(value, c.x+36, c.y+165)
} }
func filledBar(dc *gg.Context, x, y, totalW, h, pct float64, fill color.RGBA) { func filledBar(dc *gg.Context, x, y, totalW, h, pct float64, fill color.RGBA) {
@@ -142,10 +149,10 @@ func renderCommitsCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
bigNum(dc, c, formatNumber(stats.TotalCommits), bigNum(dc, c, formatNumber(stats.TotalCommits),
fmt.Sprintf("%.1f commits per day on average", stats.AverageCommitsPerDay)) fmt.Sprintf("%.1f commits per day on average", stats.AverageCommitsPerDay))
dc.DrawRoundedRectangle(c.x+24, c.y+c.h-40, c.w-48, 10, 5) dc.DrawRoundedRectangle(c.x+36, c.y+c.h-60, c.w-72, 15, 7)
dc.SetColor(color.RGBA{50, 40, 60, 255}) dc.SetColor(color.RGBA{70, 50, 90, 255})
dc.Fill() dc.Fill()
dc.DrawRoundedRectangle(c.x+24, c.y+c.h-40, (c.w-48)*0.72, 10, 5) dc.DrawRoundedRectangle(c.x+36, c.y+c.h-60, (c.w-72)*0.72, 15, 7)
dc.SetColor(c.accent) dc.SetColor(c.accent)
dc.Fill() dc.Fill()
} }
@@ -154,29 +161,29 @@ func renderReposCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
label(dc, c, "REPOSITORIES") label(dc, c, "REPOSITORIES")
bigNum(dc, c, formatNumber(stats.TotalRepositories), "worked on this year") bigNum(dc, c, formatNumber(stats.TotalRepositories), "worked on this year")
y := c.y + 160 y := c.y + 240
maxW := c.w - 48 maxW := c.w - 72
for i, repo := range stats.TopRepositories { for i, repo := range stats.TopRepositories {
if i >= 3 { if i >= 3 {
break break
} }
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textMuted) dc.SetColor(textMuted)
dc.DrawString(truncate(repo.Name, maxW, dc), c.x+24, y) dc.DrawString(truncate(repo.Name, maxW, dc), c.x+36, y)
y += 24 y += 36
} }
} }
func renderActiveDayCard(dc *gg.Context, c bentoCard, stats *models.UserStats) { func renderActiveDayCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
label(dc, c, "MOST ACTIVE DAY") label(dc, c, "MOST ACTIVE DAY")
bigText(dc, c, stats.MostActiveDay, 48) bigText(dc, c, stats.MostActiveDay, 72)
if len(stats.CommitsByWeekday) > 0 { if len(stats.CommitsByWeekday) > 0 {
loadFont(dc, 14) loadFont(dc, 21)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString( dc.DrawString(
fmt.Sprintf("%d commits that day", stats.CommitsByWeekday[0].Count), fmt.Sprintf("%d commits that day", stats.CommitsByWeekday[0].Count),
c.x+24, c.y+136, c.x+36, c.y+204,
) )
} }
} }
@@ -188,7 +195,7 @@ func renderAvgCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
func renderActiveMonthCard(dc *gg.Context, c bentoCard, stats *models.UserStats) { func renderActiveMonthCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
label(dc, c, "MOST ACTIVE MONTH") label(dc, c, "MOST ACTIVE MONTH")
bigText(dc, c, stats.MostActiveMonth, 44) bigText(dc, c, stats.MostActiveMonth, 66)
} }
func renderTopRepoCard(dc *gg.Context, c bentoCard, stats *models.UserStats) { func renderTopRepoCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
@@ -199,25 +206,25 @@ func renderTopRepoCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
} }
top := stats.TopRepositories[0] top := stats.TopRepositories[0]
maxW := c.w - 48 maxW := c.w - 72
loadFont(dc, 32) loadFont(dc, 48)
dc.SetColor(c.accent) dc.SetColor(c.accent)
dc.DrawString(truncate(top.Name, maxW, dc), c.x+24, c.y+100) dc.DrawString(truncate(top.Name, maxW, dc), c.x+36, c.y+150)
loadFont(dc, 13) loadFont(dc, 19)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString("your #1 repository this year", c.x+24, c.y+126) dc.DrawString("your #1 repository this year", c.x+36, c.y+189)
y := c.y + 164 y := c.y + 246
for i := 1; i < len(stats.TopRepositories) && i < 4; i++ { for i := 1; i < len(stats.TopRepositories) && i < 4; i++ {
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textMuted) dc.SetColor(textMuted)
dc.DrawString( dc.DrawString(
fmt.Sprintf("#%d %s", i+1, truncate(stats.TopRepositories[i].Name, maxW-30, dc)), fmt.Sprintf("#%d %s", i+1, truncate(stats.TopRepositories[i].Name, maxW-45, dc)),
c.x+24, y, c.x+36, y,
) )
y += 24 y += 36
} }
} }
@@ -225,24 +232,24 @@ func renderLanguagesCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
label(dc, c, "LANGUAGES") label(dc, c, "LANGUAGES")
if len(stats.Languages) == 0 { if len(stats.Languages) == 0 {
loadFont(dc, 15) loadFont(dc, 22)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString("No language data available.", c.x+24, c.y+100) dc.DrawString("No language data available.", c.x+36, c.y+150)
return return
} }
const ( const (
numCols = 2 numCols = 2
rowsPerCol = 5 rowsPerCol = 5
rowH = 48.0 rowH = 72.0
startY = 60.0 startY = 90.0
labelW = 110.0 labelW = 165.0
countW = 90.0 countW = 135.0
barH = 14.0 barH = 21.0
) )
maxCount := stats.Languages[0].Count maxCount := stats.Languages[0].Count
halfW := (c.w - 48 - gridGap) / 2 halfW := (c.w - 72 - gridGap) / 2
for i, lang := range stats.Languages { for i, lang := range stats.Languages {
if i >= numCols*rowsPerCol { if i >= numCols*rowsPerCol {
@@ -252,23 +259,23 @@ func renderLanguagesCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
colIdx := i / rowsPerCol colIdx := i / rowsPerCol
rowIdx := i % rowsPerCol rowIdx := i % rowsPerCol
baseX := c.x + 24 + float64(colIdx)*(halfW+float64(gridGap)) baseX := c.x + 36 + float64(colIdx)*(halfW+float64(gridGap))
baseY := c.y + startY + float64(rowIdx)*rowH baseY := c.y + startY + float64(rowIdx)*rowH
pct := float64(lang.Count) / float64(maxCount) pct := float64(lang.Count) / float64(maxCount)
barW := halfW - labelW - countW - 10 barW := halfW - labelW - countW - 15
loadFont(dc, 13) loadFont(dc, 19)
dc.SetColor(textPrimary) dc.SetColor(textPrimary)
dc.DrawString(truncatePx(lang.Language, labelW-8, dc), baseX, baseY+12) dc.DrawString(truncatePx(lang.Language, labelW-12, dc), baseX, baseY+18)
filledBar(dc, baseX+labelW, baseY+2, barW, barH, pct, c.accent) filledBar(dc, baseX+labelW, baseY+3, barW, barH, pct, c.accent)
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString( dc.DrawString(
fmt.Sprintf("%d %.1f%%", lang.Count, lang.Percent), fmt.Sprintf("%d %.1f%%", lang.Count, lang.Percent),
baseX+labelW+barW+10, baseY+13, baseX+labelW+barW+15, baseY+19,
) )
} }
} }
@@ -277,69 +284,69 @@ func renderActivityCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
label(dc, c, "ACTIVITY PATTERNS") label(dc, c, "ACTIVITY PATTERNS")
const ( const (
startY = 54.0 startY = 81.0
rowH = 28.0 rowH = 42.0
labelW = 44.0 labelW = 66.0
countW = 50.0 countW = 75.0
barH = 14.0 barH = 21.0
) )
halfW := (c.w - 48 - float64(gridGap)) / 2 halfW := (c.w - 72 - float64(gridGap)) / 2
lx := c.x + 24 lx := c.x + 36
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString("By Weekday", lx, c.y+startY) dc.DrawString("By Weekday", lx, c.y+startY)
if len(stats.CommitsByWeekday) > 0 { if len(stats.CommitsByWeekday) > 0 {
maxC := stats.CommitsByWeekday[0].Count maxC := stats.CommitsByWeekday[0].Count
barW := halfW - labelW - countW - 10 barW := halfW - labelW - countW - 15
for i, day := range stats.CommitsByWeekday { for i, day := range stats.CommitsByWeekday {
if i >= 7 { if i >= 7 {
break break
} }
by := c.y + startY + 24 + float64(i)*rowH by := c.y + startY + 36 + float64(i)*rowH
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textPrimary) dc.SetColor(textPrimary)
dc.DrawString(day.Day[:3], lx, by+12) dc.DrawString(day.Day[:3], lx, by+18)
pct := float64(day.Count) / float64(maxC) pct := float64(day.Count) / float64(maxC)
filledBar(dc, lx+labelW, by+2, barW, barH, pct, accentCyan) filledBar(dc, lx+labelW, by+3, barW, barH, pct, accentCyan)
loadFont(dc, 11) loadFont(dc, 16)
dc.SetColor(textMuted) dc.SetColor(textMuted)
dc.DrawString(formatNumber(day.Count), lx+labelW+barW+8, by+12) dc.DrawString(formatNumber(day.Count), lx+labelW+barW+12, by+18)
} }
} }
rx := c.x + 24 + halfW + float64(gridGap) rx := c.x + 36 + halfW + float64(gridGap)
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textSecondary) dc.SetColor(textSecondary)
dc.DrawString("By Month", rx, c.y+startY) dc.DrawString("By Month", rx, c.y+startY)
if len(stats.CommitsByMonth) > 0 { if len(stats.CommitsByMonth) > 0 {
maxC := stats.CommitsByMonth[0].Count maxC := stats.CommitsByMonth[0].Count
barW := halfW - labelW - countW - 10 barW := halfW - labelW - countW - 15
for i, month := range stats.CommitsByMonth { for i, month := range stats.CommitsByMonth {
if i >= 7 { if i >= 7 {
break break
} }
by := c.y + startY + 24 + float64(i)*rowH by := c.y + startY + 36 + float64(i)*rowH
loadFont(dc, 12) loadFont(dc, 18)
dc.SetColor(textPrimary) dc.SetColor(textPrimary)
dc.DrawString(month.Date.Format("Jan"), rx, by+12) dc.DrawString(month.Date.Format("Jan"), rx, by+18)
pct := float64(month.Count) / float64(maxC) pct := float64(month.Count) / float64(maxC)
filledBar(dc, rx+labelW, by+2, barW, barH, pct, accentOrange) filledBar(dc, rx+labelW, by+3, barW, barH, pct, accentOrange)
loadFont(dc, 11) loadFont(dc, 16)
dc.SetColor(textMuted) dc.SetColor(textMuted)
dc.DrawString(formatNumber(month.Count), rx+labelW+barW+8, by+12) dc.DrawString(formatNumber(month.Count), rx+labelW+barW+12, by+18)
} }
} }
} }