package renderer import ( "bytes" "encoding/binary" "image" "image/color" "image/png" "math" "runtime" "github.com/fogleman/gg" ) const iconSize = 64 // Claude orange for the starburst logo. var claudeOrange = color.RGBA{224, 123, 83, 255} // arcColors maps usage percentage thresholds to colors. var arcColors = []struct { threshold int color color.RGBA }{ {10, color.RGBA{76, 175, 80, 255}}, // green {20, color.RGBA{67, 160, 71, 255}}, // darker green {30, color.RGBA{124, 179, 66, 255}}, // light green {40, color.RGBA{192, 202, 51, 255}}, // lime {50, color.RGBA{253, 216, 53, 255}}, // yellow {60, color.RGBA{255, 193, 7, 255}}, // amber {70, color.RGBA{255, 179, 0, 255}}, // darker amber {80, color.RGBA{255, 152, 0, 255}}, // orange {90, color.RGBA{255, 87, 34, 255}}, // deep orange {100, color.RGBA{244, 67, 54, 255}}, // red } func getArcColor(pct int) color.RGBA { for _, ac := range arcColors { if pct <= ac.threshold { return ac.color } } return arcColors[len(arcColors)-1].color } // drawStarburst draws the 8-petal Claude logo. func drawStarburst(dc *gg.Context) { cx := float64(iconSize) / 2 cy := float64(iconSize) / 2 petalLen := float64(iconSize) * 0.38 petalWidth := float64(iconSize) * 0.10 centerRadius := float64(iconSize) * 0.04 dc.SetColor(claudeOrange) for i := 0; i < 8; i++ { angle := float64(i) * (2 * math.Pi / 8) // Tip of the petal tipX := cx + petalLen*math.Cos(angle) tipY := cy + petalLen*math.Sin(angle) // Base points (perpendicular to angle) perpAngle := angle + math.Pi/2 baseX1 := cx + petalWidth*math.Cos(perpAngle) baseY1 := cy + petalWidth*math.Sin(perpAngle) baseX2 := cx - petalWidth*math.Cos(perpAngle) baseY2 := cy - petalWidth*math.Sin(perpAngle) // Inner point (slightly behind center for petal shape) innerX := cx - petalWidth*0.5*math.Cos(angle) innerY := cy - petalWidth*0.5*math.Sin(angle) dc.MoveTo(innerX, innerY) dc.LineTo(baseX1, baseY1) dc.LineTo(tipX, tipY) dc.LineTo(baseX2, baseY2) dc.ClosePath() dc.Fill() } // Center dot dc.DrawCircle(cx, cy, centerRadius) dc.Fill() } // drawArc draws a circular progress arc. func drawArc(dc *gg.Context, pct int) { if pct <= 0 { return } if pct > 100 { pct = 100 } cx := float64(iconSize) / 2 cy := float64(iconSize) / 2 radius := float64(iconSize)/2 - 4 // inset from edge arcWidth := 7.0 startAngle := -math.Pi / 2 // 12 o'clock endAngle := startAngle + (float64(pct)/100)*2*math.Pi dc.SetColor(getArcColor(pct)) dc.SetLineWidth(arcWidth) dc.SetLineCap(gg.LineCapButt) dc.DrawArc(cx, cy, radius, startAngle, endAngle) dc.Stroke() } // RenderIcon generates a 256x256 PNG icon with starburst and usage arc. func RenderIcon(pct int) image.Image { dc := gg.NewContext(iconSize, iconSize) drawStarburst(dc) drawArc(dc, pct) return dc.Image() } // RenderIconPNG generates the icon as PNG bytes. func RenderIconPNG(pct int) ([]byte, error) { img := RenderIcon(pct) var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { return nil, err } return buf.Bytes(), nil } // RenderIconForTray returns icon bytes suitable for systray.SetIcon: // ICO (PNG-compressed) on Windows, raw PNG on other platforms. func RenderIconForTray(pct int) ([]byte, error) { pngData, err := RenderIconPNG(pct) if err != nil { return nil, err } if runtime.GOOS != "windows" { return pngData, nil } return wrapPNGInICO(pngData, iconSize, iconSize), nil } // wrapPNGInICO wraps raw PNG bytes in a minimal ICO container. // Windows Vista+ supports PNG-compressed ICO entries. func wrapPNGInICO(pngData []byte, width, height int) []byte { const headerSize = 6 const entrySize = 16 imageOffset := headerSize + entrySize buf := new(bytes.Buffer) // ICONDIR header binary.Write(buf, binary.LittleEndian, uint16(0)) // Reserved binary.Write(buf, binary.LittleEndian, uint16(1)) // Type: 1 = icon binary.Write(buf, binary.LittleEndian, uint16(1)) // Count: 1 image // ICONDIRENTRY w := byte(width) if width >= 256 { w = 0 // 0 means 256 } h := byte(height) if height >= 256 { h = 0 } buf.WriteByte(w) // Width buf.WriteByte(h) // Height buf.WriteByte(0) // ColorCount (0 = no palette) buf.WriteByte(0) // Reserved binary.Write(buf, binary.LittleEndian, uint16(1)) // Planes binary.Write(buf, binary.LittleEndian, uint16(32)) // BitCount binary.Write(buf, binary.LittleEndian, uint32(len(pngData))) // BytesInRes binary.Write(buf, binary.LittleEndian, uint32(imageOffset)) // ImageOffset // PNG image data buf.Write(pngData) return buf.Bytes() }