229 lines
4.8 KiB
Go
229 lines
4.8 KiB
Go
package image_table
|
||
|
||
import (
|
||
_ "embed"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"math"
|
||
"reflect"
|
||
|
||
"golang.org/x/image/font"
|
||
"golang.org/x/image/font/opentype"
|
||
"golang.org/x/image/math/fixed"
|
||
)
|
||
|
||
//go:embed assets/jb.ttf
|
||
var jbTTF []byte
|
||
|
||
var globalFace font.Face
|
||
|
||
// ---- Цвета ----
|
||
var (
|
||
headerBg = color.RGBA{0xD8, 0xEC, 0xFF, 255} // голубой фон заголовка
|
||
rowBg1 = color.RGBA{0xFF, 0xFF, 0xFF, 255} // белый фон строки 1
|
||
rowBg2 = color.RGBA{0xF7, 0xFA, 0xFC, 255} // мягкий фон строки 2
|
||
borderCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // рамка
|
||
dividerCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // линии
|
||
textCol = color.RGBA{0x34, 0x34, 0x32, 255} // тёплый тёмно-серый текст
|
||
)
|
||
|
||
// ---- Инициализация шрифта ----
|
||
|
||
func init() {
|
||
f, err := opentype.Parse(jbTTF)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
globalFace, err = opentype.NewFace(f, &opentype.FaceOptions{
|
||
Size: 10,
|
||
DPI: 96,
|
||
Hinting: font.HintingFull,
|
||
})
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
|
||
// ---- Утилиты ----
|
||
|
||
func toString(v any) string {
|
||
return fmt.Sprintf("%v", v)
|
||
}
|
||
|
||
func rowToStrings(v any) []string {
|
||
val := reflect.ValueOf(v)
|
||
|
||
switch val.Kind() {
|
||
case reflect.Slice, reflect.Array:
|
||
out := make([]string, val.Len())
|
||
for i := 0; i < val.Len(); i++ {
|
||
out[i] = toString(val.Index(i).Interface())
|
||
}
|
||
return out
|
||
|
||
case reflect.Struct:
|
||
out := make([]string, val.NumField())
|
||
for i := 0; i < val.NumField(); i++ {
|
||
out[i] = toString(val.Field(i).Interface())
|
||
}
|
||
return out
|
||
}
|
||
|
||
return []string{toString(v)}
|
||
}
|
||
|
||
func drawText(img *image.RGBA, x, y int, text string) {
|
||
d := &font.Drawer{
|
||
Dst: img,
|
||
Src: image.NewUniform(textCol),
|
||
Face: globalFace,
|
||
Dot: fixed.P(x, y),
|
||
}
|
||
d.DrawString(text)
|
||
}
|
||
|
||
func measureText(s string) int {
|
||
d := &font.Drawer{Face: globalFace}
|
||
return d.MeasureString(s).Round()
|
||
}
|
||
|
||
func roundedRect(img *image.RGBA, rect image.Rectangle, r int, col color.Color) {
|
||
w := rect.Dx()
|
||
h := rect.Dy()
|
||
|
||
for y := 0; y < h; y++ {
|
||
for x := 0; x < w; x++ {
|
||
if (x < r && y < r && dist(x, y, r, r) > float64(r)) ||
|
||
(x >= w-r && y < r && dist(x, y, w-r-1, r) > float64(r)) ||
|
||
(x < r && y >= h-r && dist(x, y, r, h-r-1) > float64(r)) ||
|
||
(x >= w-r && y >= h-r && dist(x, y, w-r-1, h-r-1) > float64(r)) {
|
||
continue
|
||
}
|
||
img.Set(rect.Min.X+x, rect.Min.Y+y, col)
|
||
}
|
||
}
|
||
}
|
||
|
||
func dist(x1, y1, x2, y2 int) float64 {
|
||
return math.Hypot(float64(x1-x2), float64(y1-y2))
|
||
}
|
||
|
||
// ---- Основная функция ----
|
||
|
||
func DrawTableWarm(header []string, rows []any) image.Image {
|
||
padX := 8
|
||
padY := 10
|
||
|
||
rowHeight := 26
|
||
headerHeight := 32
|
||
|
||
colCount := len(header)
|
||
colWidths := make([]int, colCount)
|
||
|
||
// ---- Автоширина ----
|
||
|
||
for i, h := range header {
|
||
w := measureText(h)
|
||
if w > colWidths[i] {
|
||
colWidths[i] = w
|
||
}
|
||
}
|
||
|
||
for _, r := range rows {
|
||
cells := rowToStrings(r)
|
||
for i := 0; i < len(cells) && i < colCount; i++ {
|
||
w := measureText(cells[i])
|
||
if w > colWidths[i] {
|
||
colWidths[i] = w
|
||
}
|
||
}
|
||
}
|
||
|
||
for i := range colWidths {
|
||
colWidths[i] += padX * 2
|
||
}
|
||
|
||
imgWidth := 0
|
||
for _, w := range colWidths {
|
||
imgWidth += w
|
||
}
|
||
|
||
imgHeight := headerHeight + len(rows)*rowHeight
|
||
|
||
img := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
|
||
|
||
// общий фон
|
||
draw.Draw(img, img.Bounds(), &image.Uniform{rowBg1}, image.Point{}, draw.Src)
|
||
|
||
// ---- Заголовок ----
|
||
roundedRect(img, image.Rect(0, 0, imgWidth, headerHeight), 8, headerBg)
|
||
|
||
// Горизонтальная линия под заголовком
|
||
for xx := 0; xx < imgWidth; xx++ {
|
||
img.Set(xx, headerHeight-1, dividerCol)
|
||
}
|
||
|
||
x := 0
|
||
for i, h := range header {
|
||
drawText(img, x+padX, headerHeight-padY, h)
|
||
x += colWidths[i]
|
||
|
||
// Вертикальный разделитель
|
||
for yy := 0; yy < headerHeight; yy++ {
|
||
img.Set(x, yy, dividerCol)
|
||
}
|
||
}
|
||
|
||
// ---- Строки ----
|
||
y := headerHeight
|
||
for rowIndex, row := range rows {
|
||
bg := rowBg1
|
||
if rowIndex%2 == 1 {
|
||
bg = rowBg2
|
||
}
|
||
|
||
// фон строки
|
||
for xx := 0; xx < imgWidth; xx++ {
|
||
for yy := 0; yy < rowHeight; yy++ {
|
||
img.Set(xx, y+yy, bg)
|
||
}
|
||
}
|
||
|
||
// горизонтальный разделитель ВВЕРХУ строки
|
||
for xx := 0; xx < imgWidth; xx++ {
|
||
img.Set(xx, y, dividerCol)
|
||
}
|
||
|
||
cells := rowToStrings(row)
|
||
|
||
x = 0
|
||
for i := 0; i < colCount && i < len(cells); i++ {
|
||
drawText(img, x+padX, y+rowHeight-padY, cells[i])
|
||
|
||
x += colWidths[i]
|
||
|
||
// вертикальная линия
|
||
for yy := 0; yy < rowHeight; yy++ {
|
||
img.Set(x, y+yy, dividerCol)
|
||
}
|
||
}
|
||
|
||
y += rowHeight
|
||
}
|
||
|
||
// Рамка
|
||
for xx := 0; xx < imgWidth; xx++ {
|
||
img.Set(xx, 0, borderCol)
|
||
img.Set(xx, imgHeight-1, borderCol)
|
||
}
|
||
for yy := 0; yy < imgHeight; yy++ {
|
||
img.Set(0, yy, borderCol)
|
||
img.Set(imgWidth-1, yy, borderCol)
|
||
}
|
||
|
||
return img
|
||
}
|