spark-server/internal/module/group/entity/collage.go
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:11 +08:00

106 lines
2.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package entity
import (
"image"
"image/color"
"image/draw"
)
const (
collageSize = 480
collageGap = 8
)
var (
collageBG = color.RGBA{R: 237, G: 237, B: 237, A: 255}
collageFill = color.RGBA{R: 216, G: 216, B: 216, A: 255}
)
// PlaceholderFace is a solid tile used when a member avatar cannot be loaded.
func PlaceholderFace() *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, 256, 256))
draw.Draw(img, img.Bounds(), &image.Uniform{C: collageFill}, image.Point{}, draw.Src)
return img
}
// Collage tiles as many member avatars as given (1–9). Two people are two tiles, not a 3×3 grid.
func Collage(faces []image.Image) *image.RGBA {
if len(faces) > MaxCollageFaces {
faces = faces[:MaxCollageFaces]
}
dst := image.NewRGBA(image.Rect(0, 0, collageSize, collageSize))
draw.Draw(dst, dst.Bounds(), &image.Uniform{C: collageBG}, image.Point{}, draw.Src)
cells := collageCells(len(faces))
for i := range cells {
r := cells[i]
src := image.Image(&image.Uniform{C: collageFill})
if i < len(faces) && faces[i] != nil {
src = faces[i]
}
blitSquare(dst, src, r)
}
return dst
}
func collageCells(n int) []image.Rectangle {
if n < 1 {
return nil
}
if n > MaxCollageFaces {
n = MaxCollageFaces
}
cols := 1
if n >= 5 {
cols = 3
} else if n >= 2 {
cols = 2
}
rows := (n + cols - 1) / cols
cell := (collageSize - collageGap*(cols+1)) / cols
gridW := cols*cell + collageGap*(cols-1)
gridH := rows*cell + collageGap*(rows-1)
ox := (collageSize - gridW) / 2
oy := (collageSize - gridH) / 2
first := n % cols
if first == 0 {
first = cols
}
out := make([]image.Rectangle, 0, n)
i := 0
for row := 0; row < rows && i < n; row++ {
count := cols
if row == 0 {
count = first
}
rowW := count*cell + collageGap*(count-1)
x0 := ox + (gridW-rowW)/2
y := oy + row*(cell+collageGap)
for c := 0; c < count && i < n; c++ {
x := x0 + c*(cell+collageGap)
out = append(out, image.Rect(x, y, x+cell, y+cell))
i++
}
}
return out
}
func blitSquare(dst *image.RGBA, src image.Image, r image.Rectangle) {
sb := src.Bounds()
side := sb.Dx()
if h := sb.Dy(); h < side {
side = h
}
if side < 1 || r.Empty() {
return
}
sx0 := sb.Min.X + (sb.Dx()-side)/2
sy0 := sb.Min.Y + (sb.Dy()-side)/2
w, h := r.Dx(), r.Dy()
for y := 0; y < h; y++ {
sy := sy0 + y*side/h
for x := 0; x < w; x++ {
dst.Set(r.Min.X+x, r.Min.Y+y, src.At(sx0+x*side/w, sy))
}
}
}