179 lines
3.6 KiB
Go
179 lines
3.6 KiB
Go
package rankcard
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/fogleman/gg"
|
|
"golang.org/x/image/draw"
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/font/gofont/goregular"
|
|
"golang.org/x/image/font/opentype"
|
|
)
|
|
|
|
type Data struct {
|
|
Name string
|
|
Level int
|
|
XP int64
|
|
Percent float64 // 0..1 progress towards next level
|
|
Avatar string // URL
|
|
}
|
|
|
|
func RenderPNG(ctx context.Context, d Data) ([]byte, error) {
|
|
const (
|
|
w = 900
|
|
h = 300
|
|
px = 30
|
|
|
|
avatarSize = 150
|
|
barX = 30
|
|
barY = 220
|
|
barW = 650
|
|
barH = 40
|
|
)
|
|
|
|
percent := clamp01(d.Percent)
|
|
|
|
dc := gg.NewContext(w, h)
|
|
dc.SetHexColor("#141414")
|
|
dc.Clear()
|
|
|
|
// Right white polygon
|
|
dc.SetHexColor("#FFFFFF")
|
|
dc.NewSubPath()
|
|
dc.MoveTo(600, 0)
|
|
dc.LineTo(750, 300)
|
|
dc.LineTo(900, 300)
|
|
dc.LineTo(900, 0)
|
|
dc.ClosePath()
|
|
dc.Fill()
|
|
|
|
// Avatar (circle clip)
|
|
if img, err := fetchImage(ctx, d.Avatar); err == nil && img != nil {
|
|
img = resizeImage(img, avatarSize, avatarSize)
|
|
dc.Push()
|
|
dc.DrawCircle(px+avatarSize/2, px+avatarSize/2, avatarSize/2)
|
|
dc.Clip()
|
|
dc.DrawImage(img, px, px)
|
|
dc.Pop()
|
|
// Defensive: ensure no clip leaks past avatar drawing.
|
|
dc.ResetClip()
|
|
}
|
|
|
|
// Progress bar background
|
|
dc.SetHexColor("#FFFFFF")
|
|
dc.DrawRoundedRectangle(barX, barY, barW, barH, 20)
|
|
dc.Fill()
|
|
|
|
// Progress bar fill
|
|
fillW := barW * percent
|
|
if fillW > 0 {
|
|
dc.SetHexColor("#282828")
|
|
// Keep radius from looking weird on small widths
|
|
r := math.Min(20, fillW/2)
|
|
dc.DrawRoundedRectangle(barX, barY, fillW, barH, r)
|
|
dc.Fill()
|
|
}
|
|
|
|
// Text
|
|
titleFace, smallFace, err := loadFontFaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dc.SetHexColor("#FFFFFF")
|
|
dc.SetFontFace(titleFace)
|
|
dc.DrawStringAnchored(d.Name, 200, 60, 0, 0.5)
|
|
|
|
dc.SetLineWidth(2)
|
|
dc.DrawLine(200, 100, 550, 100)
|
|
dc.Stroke()
|
|
|
|
dc.SetFontFace(smallFace)
|
|
dc.DrawStringAnchored(
|
|
fmt.Sprintf("Level - %d | XP - %d | %d%%", d.Level, d.XP, int64(math.Round(percent*100))),
|
|
200,
|
|
145,
|
|
0,
|
|
0.5,
|
|
)
|
|
|
|
// Watermark to verify latest render is running
|
|
dc.SetFontFace(smallFace)
|
|
dc.SetHexColor("#FFCC00")
|
|
dc.DrawStringAnchored("velox-go rankcard", 890, 290, 1, 1)
|
|
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, dc.Image()); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func clamp01(v float64) float64 {
|
|
if v < 0 {
|
|
return 0
|
|
}
|
|
if v > 1 {
|
|
return 1
|
|
}
|
|
return v
|
|
}
|
|
|
|
func loadFontFaces() (title font.Face, small font.Face, err error) {
|
|
f, err := opentype.Parse(goregular.TTF)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
title, err = opentype.NewFace(f, &opentype.FaceOptions{Size: 40, DPI: 72, Hinting: font.HintingFull})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
small, err = opentype.NewFace(f, &opentype.FaceOptions{Size: 30, DPI: 72, Hinting: font.HintingFull})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return title, small, nil
|
|
}
|
|
|
|
func fetchImage(ctx context.Context, url string) (image.Image, error) {
|
|
if url == "" {
|
|
return nil, fmt.Errorf("empty avatar url")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
io.Copy(io.Discard, resp.Body)
|
|
return nil, fmt.Errorf("avatar fetch status %d", resp.StatusCode)
|
|
}
|
|
|
|
img, _, err := image.Decode(resp.Body)
|
|
return img, err
|
|
}
|
|
|
|
func resizeImage(src image.Image, w, h int) image.Image {
|
|
dst := image.NewRGBA(image.Rect(0, 0, w, h))
|
|
draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
|
|
return dst
|
|
}
|