feat(misc): rotate wheel beta labels

This commit is contained in:
2026-07-06 14:06:18 +07:00
parent e4b24f03e4
commit de4db1acbc
2 changed files with 89 additions and 7 deletions
+52 -2
View File
@@ -386,13 +386,32 @@ func TestWheelOfNamesBeta_DrawsOptionLabelsInsideSlices(t *testing.T) {
segment := 2 * math.Pi / float64(len(options))
angle := rotation + segment/2
centerX := wheelBetaSize/2 + int(math.Round(math.Cos(angle)*float64(wheelBetaSliceLabelRadius)))
baselineY := wheelBetaSize/2 + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius))) + wheelBetaSliceLabelBaselineOffset
bounds := image.Rect(centerX-28, baselineY-14, centerX+28, baselineY+2)
centerY := wheelBetaSize/2 + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius)))
bounds := image.Rect(centerX-28, centerY-28, centerX+28, centerY+28)
if got := countColorIndex(img, bounds, 1); got == 0 {
t.Fatalf("slice label dark pixels = %d, want > 0", got)
}
}
func TestWheelOfNamesBeta_RotatesOptionLabelsWithSlices(t *testing.T) {
options := []string{"Rotate", "Teacher", "Parent", "Staff"}
rotation := -3 * math.Pi / 4
img := renderWheelBetaFrame(options, 0, rotation, false)
segment := 2 * math.Pi / float64(len(options))
angle := rotation + segment/2
centerX := wheelBetaSize/2 + int(math.Round(math.Cos(angle)*float64(wheelBetaSliceLabelRadius)))
centerY := wheelBetaSize/2 + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius)))
searchBounds := image.Rect(centerX-28, centerY-32, centerX+28, centerY+32)
labelBounds, ok := colorIndexBounds(img, searchBounds, wheelBetaInkColorIndex)
if !ok {
t.Fatalf("slice label dark pixels missing in %v", searchBounds)
}
if labelBounds.Dy() <= labelBounds.Dx() {
t.Fatalf("slice label bounds = %v, want taller than wide for rotated label", labelBounds)
}
}
func countColorIndex(img *image.Paletted, bounds image.Rectangle, colorIndex byte) int {
bounds = bounds.Intersect(img.Bounds())
count := 0
@@ -406,6 +425,37 @@ func countColorIndex(img *image.Paletted, bounds image.Rectangle, colorIndex byt
return count
}
func colorIndexBounds(img *image.Paletted, bounds image.Rectangle, colorIndex byte) (image.Rectangle, bool) {
bounds = bounds.Intersect(img.Bounds())
minX, minY := bounds.Max.X, bounds.Max.Y
maxX, maxY := bounds.Min.X, bounds.Min.Y
found := false
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if img.ColorIndexAt(x, y) != colorIndex {
continue
}
found = true
if x < minX {
minX = x
}
if y < minY {
minY = y
}
if x+1 > maxX {
maxX = x + 1
}
if y+1 > maxY {
maxY = y + 1
}
}
}
if !found {
return image.Rectangle{}, false
}
return image.Rect(minX, minY, maxX, maxY), true
}
func TestWheelOfNamesBeta_SendsAnimationWithoutSpoilingCaption(t *testing.T) {
rb, _ := installMisc(t, 999)
rb.Bot.ProcessUpdate(context.Background(), testutil.NewPrivateMessage(7, "/wheelofnamesbeta Alice"))
@@ -2,6 +2,7 @@ package misc
import (
"image"
"image/color"
"math"
"strings"
@@ -11,8 +12,7 @@ import (
)
const (
wheelBetaSliceLabelRadius = 64
wheelBetaSliceLabelBaselineOffset = 5
wheelBetaSliceLabelRadius = 64
)
func drawWheelSliceLabels(img *image.Paletted, options []string, rotation float64) {
@@ -25,10 +25,10 @@ func drawWheelSliceLabels(img *image.Paletted, options []string, rotation float6
for idx, option := range options {
angle := rotation + (float64(idx)+0.5)*segment
centerX := cx + int(math.Round(math.Cos(angle)*float64(wheelBetaSliceLabelRadius)))
baselineY := cy + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius))) + wheelBetaSliceLabelBaselineOffset
centerY := cy + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius)))
text := asciiWheelText(option, limit)
drawCenteredTextAt(img, text, centerX+1, baselineY+1, 2)
drawCenteredTextAt(img, text, centerX, baselineY, 1)
drawRotatedCenteredTextAt(img, text, centerX+1, centerY+1, angle, wheelBetaPaperColorIndex)
drawRotatedCenteredTextAt(img, text, centerX, centerY, angle, wheelBetaInkColorIndex)
}
}
@@ -215,6 +215,38 @@ func drawCenteredTextAt(img *image.Paletted, text string, centerX, baselineY int
drawer.DrawString(text)
}
func drawRotatedCenteredTextAt(img *image.Paletted, text string, centerX, centerY int, angle float64, colorIndex byte) {
face := basicfont.Face7x13
padding := 2
metrics := face.Metrics()
textWidth := font.MeasureString(face, text).Ceil()
textHeight := metrics.Height.Ceil()
mask := image.NewAlpha(image.Rect(0, 0, textWidth+padding*2, textHeight+padding*2))
drawer := font.Drawer{
Dst: mask,
Src: image.NewUniform(color.Alpha{A: 255}),
Face: face,
Dot: fixed.P(padding, padding+metrics.Ascent.Ceil()),
}
drawer.DrawString(text)
sourceCenterX := float64(mask.Bounds().Dx()-1) / 2
sourceCenterY := float64(mask.Bounds().Dy()-1) / 2
sin, cos := math.Sin(angle), math.Cos(angle)
for y := mask.Bounds().Min.Y; y < mask.Bounds().Max.Y; y++ {
for x := mask.Bounds().Min.X; x < mask.Bounds().Max.X; x++ {
if mask.AlphaAt(x, y).A == 0 {
continue
}
localX := float64(x) - sourceCenterX
localY := float64(y) - sourceCenterY
targetX := centerX + int(math.Round(localX*cos-localY*sin))
targetY := centerY + int(math.Round(localX*sin+localY*cos))
setWheelBetaPixel(img, targetX, targetY, colorIndex)
}
}
}
func asciiWheelText(s string, limit int) string {
var b strings.Builder
for _, r := range s {