fix(misc): restore wheel beta renderer

This commit is contained in:
tiennm99 committed 2026-07-06 15:30:29 +07:00
1 parent e7154d27f3
commit 0a4d237391
12 files changed
+283 -812

No files matched your search

@@ -1,47 +0,0 @@
DejaVu Sans font license
Source: https://dejavu-fonts.github.io/
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
Bitstream Vera is a trademark of Bitstream, Inc.
DejaVu changes are in public domain.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and distributed under the "Bitstream Vera"
names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively.
Binary file not shown.
+10 -28
View File
@@ -12,7 +12,6 @@ import (
"time"
"github.com/go-telegram/bot/models"
"github.com/tdewolff/canvas"
"github.com/tiennm99/miti99bot/internal/modules"
"github.com/tiennm99/miti99bot/internal/storage"
@@ -426,39 +425,22 @@ func TestWheelOfNamesBeta_RotatesOptionLabelsWithSlices(t *testing.T) {
}
}
func TestWheelOfNamesBeta_DisplayTextPreservesVietnamese(t *testing.T) {
input := "Tiếng Việt Đặng"
got := wheelBetaDisplayText(input, 32)
if got != input {
t.Fatalf("wheelBetaDisplayText() = %q, want %q", got, input)
func TestWheelOfNamesBeta_DisplayTextNormalizesVietnamese(t *testing.T) {
input := "không dấu Tiếng Việt Đặng Ơ Ư ấ ệ"
want := "khong dau Tieng Viet Dang O U a e"
got := wheelBetaDisplayText(input, 64)
if got != want {
t.Fatalf("wheelBetaDisplayText() = %q, want %q", got, want)
}
if strings.Contains(got, "?") {
t.Fatalf("wheelBetaDisplayText() replaced Vietnamese with ?: %q", got)
}
}
func TestWheelOfNamesBeta_FontUsesNormalWeight(t *testing.T) {
if got := wheelBetaTextFont.Style().Weight(); got != canvas.FontRegular {
t.Fatalf("font weight = %v, want regular non-bold face", got)
}
if wheelBetaTextFont.Style().Italic() {
t.Fatalf("font style = %v, want non-italic regular face", wheelBetaTextFont.Style())
}
}
func TestWheelOfNamesBeta_FontSupportsVietnameseGlyphs(t *testing.T) {
face := newWheelBetaTextFace(wheelBetaInkColorIndex)
for _, r := range "Tiếng Việt Đặng Ơ Ư ấ ệ" {
if r == ' ' {
continue
}
glyphs := face.Glyphs(string(r))
if len(glyphs) == 0 {
t.Fatalf("Glyphs(%q) is empty, want Vietnamese glyph support", r)
}
if glyphs[0].ID == 0 {
t.Fatalf("Glyphs(%q)[0].ID = 0, want Vietnamese glyph support", r)
}
func TestWheelOfNamesBeta_DisplayTextNormalizesDecomposedVietnamese(t *testing.T) {
got := wheelBetaDisplayText("tie\u0302\u0301ng Vie\u0323t", 32)
if got != "tieng Viet" {
t.Fatalf("wheelBetaDisplayText() = %q, want %q", got, "tieng Viet")
}
}
+33 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"image"
"image/color"
"image/draw"
"image/gif"
"math"
)
@@ -103,8 +104,38 @@ func renderWheelBetaFrameWithStatus(options []string, winner int, rotation float
}
func renderWheelBetaFrameWithCelebration(options []string, winner int, rotation float64, reveal bool, status string, celebrateStep int) *image.Paletted {
rect := image.Rect(0, 0, wheelBetaSize, wheelBetaSize)
img := image.NewPaletted(rect, wheelBetaPalette)
draw.Draw(img, rect, image.NewUniform(wheelBetaPalette[wheelBetaBackgroundColorIndex]), image.Point{}, draw.Src)
cx, cy := wheelBetaSize/2, wheelBetaSize/2
drawWheelDropShadow(img, cx, cy)
segment := 2 * math.Pi / float64(len(options))
r2 := wheelBetaRadius * wheelBetaRadius
for y := cy - wheelBetaRadius; y <= cy+wheelBetaRadius; y++ {
for x := cx - wheelBetaRadius; x <= cx+wheelBetaRadius; x++ {
dx, dy := x-cx, y-cy
if dx*dx+dy*dy > r2 {
continue
}
theta := normalizeAngle(math.Atan2(float64(dy), float64(dx)) - rotation)
idx := int(theta / segment)
colorIndex := wheelBetaSliceColorIndexes[idx%len(wheelBetaSliceColorIndexes)]
img.SetColorIndex(x, y, colorIndex)
}
}
currentIndex := currentWheelBetaIndex(len(options), rotation)
pointerColor := wheelBetaSliceColorIndexes[currentIndex%len(wheelBetaSliceColorIndexes)]
drawWheelLighting(img, cx, cy)
drawWheelRim(img, cx, cy)
drawWinnerCelebration(img, celebrateStep)
drawWheelSliceLabels(img, options, rotation)
drawCircle(img, cx, cy, 24, wheelBetaPaperColorIndex)
drawCircle(img, cx, cy, 18, wheelBetaInkColorIndex)
drawPointer(img, cx+wheelBetaRadius, cy, pointerColor)
drawCenteredText(img, "WHEELOFNAMES BETA", cy+wheelBetaRadius+34, wheelBetaInkColorIndex)
label := status
if label == "" {
label = "CURRENT"
@@ -114,7 +145,8 @@ func renderWheelBetaFrameWithCelebration(options []string, winner int, rotation
label = "RESULT"
value = wheelBetaDisplayText(options[winner], 28)
}
return palettizeWheelBetaFrame(renderWheelBetaCanvasImage(options, rotation, pointerColor, label, value, celebrateStep))
drawStatusBand(img, label, value)
return img
}
func finalWheelRotation(optionCount, winner int) float64 {
+236 -192
View File
@@ -1,121 +1,36 @@
package misc
import (
_ "embed"
"image"
"image/draw"
"image/color"
"math"
"strings"
"unicode"
"github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/rasterizer"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/fixed"
"golang.org/x/text/unicode/norm"
)
const (
wheelBetaSliceLabelRadius = 64
wheelBetaFontSize = 10
)
//go:embed fonts/dejavu-sans.ttf
var wheelBetaTextFontBytes []byte
var (
wheelBetaTextFont = mustWheelBetaTextFont()
wheelBetaTextFontFamily = mustWheelBetaTextFontFamily()
)
func mustWheelBetaTextFont() *canvas.Font {
parsed, err := canvas.LoadFont(wheelBetaTextFontBytes, 0, canvas.FontRegular)
if err != nil {
panic(err)
}
return parsed
}
func mustWheelBetaTextFontFamily() *canvas.FontFamily {
family := canvas.NewFontFamily("wheel-beta")
if err := family.LoadFont(wheelBetaTextFontBytes, 0, canvas.FontRegular); err != nil {
panic(err)
}
return family
}
func newWheelBetaTextFace(colorIndex byte) *canvas.FontFace {
return wheelBetaTextFontFamily.Face(wheelBetaFontSize, wheelBetaPalette[colorIndex], canvas.FontRegular)
}
func renderWheelBetaCanvasImage(options []string, rotation float64, pointerColorIndex byte, label, value string, celebrateStep int) image.Image {
c := canvas.New(float64(wheelBetaSize), float64(wheelBetaSize))
ctx := canvas.NewContext(c)
ctx.SetCoordSystem(canvas.CartesianIV)
ctx.SetStrokeCapper(canvas.RoundCap)
ctx.SetStrokeJoiner(canvas.RoundJoin)
drawWheelBetaRect(ctx, 0, 0, float64(wheelBetaSize), float64(wheelBetaSize), wheelBetaBackgroundColorIndex)
cx := float64(wheelBetaSize) / 2
cy := float64(wheelBetaSize) / 2
drawWheelDropShadow(ctx, cx, cy)
drawWheelSlices(ctx, options, rotation)
drawWheelLighting(ctx, cx, cy)
drawWheelRim(ctx, cx, cy)
drawWinnerCelebration(ctx, celebrateStep)
drawWheelSliceLabels(ctx, options, rotation)
drawCircle(ctx, cx, cy, 24, wheelBetaPaperColorIndex)
drawCircle(ctx, cx, cy, 18, wheelBetaInkColorIndex)
drawPointer(ctx, cx+float64(wheelBetaRadius), cy, pointerColorIndex)
drawCenteredText(ctx, "WHEELOFNAMES BETA", cy+float64(wheelBetaRadius)+24, wheelBetaInkColorIndex)
drawStatusBand(ctx, label, value)
return rasterizer.Draw(c, canvas.DPMM(1), canvas.DefaultColorSpace)
}
func palettizeWheelBetaFrame(src image.Image) *image.Paletted {
rect := image.Rect(0, 0, wheelBetaSize, wheelBetaSize)
img := image.NewPaletted(rect, wheelBetaPalette)
draw.FloydSteinberg.Draw(img, rect, src, image.Point{})
return img
}
func drawWheelSlices(ctx *canvas.Context, options []string, rotation float64) {
func drawWheelSliceLabels(img *image.Paletted, options []string, rotation float64) {
if len(options) == 0 {
return
}
cx := float64(wheelBetaSize) / 2
cy := float64(wheelBetaSize) / 2
segment := 2 * math.Pi / float64(len(options))
steps := int(math.Ceil(float64(wheelBetaRadius) * segment / 4))
if steps < 8 {
steps = 8
}
for idx := range options {
start := rotation + float64(idx)*segment
path := &canvas.Path{}
path.MoveTo(cx, cy)
for step := 0; step <= steps; step++ {
angle := start + segment*float64(step)/float64(steps)
path.LineTo(cx+math.Cos(angle)*float64(wheelBetaRadius), cy+math.Sin(angle)*float64(wheelBetaRadius))
}
path.Close()
drawWheelBetaPath(ctx, path, wheelBetaSliceColorIndexes[idx%len(wheelBetaSliceColorIndexes)])
}
}
func drawWheelSliceLabels(ctx *canvas.Context, options []string, rotation float64) {
if len(options) == 0 {
return
}
cx := float64(wheelBetaSize) / 2
cy := float64(wheelBetaSize) / 2
cx, cy := wheelBetaSize/2, wheelBetaSize/2
segment := 2 * math.Pi / float64(len(options))
limit := wheelBetaSliceLabelLimit(len(options))
for idx, option := range options {
angle := rotation + (float64(idx)+0.5)*segment
centerX := cx + math.Cos(angle)*float64(wheelBetaSliceLabelRadius)
centerY := cy + math.Sin(angle)*float64(wheelBetaSliceLabelRadius)
centerX := cx + int(math.Round(math.Cos(angle)*float64(wheelBetaSliceLabelRadius)))
centerY := cy + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius)))
text := wheelBetaDisplayText(option, limit)
drawRotatedCenteredTextAt(ctx, text, centerX, centerY, angle, wheelBetaInkColorIndex)
drawRotatedCenteredTextAt(img, text, centerX+1, centerY+1, angle, wheelBetaPaperColorIndex)
drawRotatedCenteredTextAt(img, text, centerX, centerY, angle, wheelBetaInkColorIndex)
}
}
@@ -132,130 +47,229 @@ func wheelBetaSliceLabelLimit(optionCount int) int {
}
}
func drawCircle(ctx *canvas.Context, cx, cy, radius float64, colorIndex byte) {
ctx.SetFillColor(wheelBetaPalette[colorIndex])
ctx.DrawPath(cx, cy, canvas.Circle(radius))
ctx.Fill()
func drawCircle(img *image.Paletted, cx, cy, radius int, colorIndex byte) {
r2 := radius * radius
for y := cy - radius; y <= cy+radius; y++ {
for x := cx - radius; x <= cx+radius; x++ {
dx, dy := x-cx, y-cy
if dx*dx+dy*dy <= r2 {
img.SetColorIndex(x, y, colorIndex)
}
}
}
}
func drawWheelDropShadow(ctx *canvas.Context, cx, cy float64) {
ctx.SetFillColor(wheelBetaPalette[wheelBetaShadowColorIndex])
ctx.DrawPath(cx, cy+8, canvas.Ellipse(float64(wheelBetaRadius+9), float64(wheelBetaRadius+5)))
ctx.Fill()
func drawWheelDropShadow(img *image.Paletted, cx, cy int) {
rx := wheelBetaRadius + 9
ry := wheelBetaRadius + 5
shadowCY := cy + 8
limit := rx * rx * ry * ry
for y := shadowCY - ry; y <= shadowCY+ry; y++ {
for x := cx - rx; x <= cx+rx; x++ {
dx := x - cx
dy := y - shadowCY
if dx*dx*ry*ry+dy*dy*rx*rx <= limit {
setWheelBetaPixel(img, x, y, wheelBetaShadowColorIndex)
}
}
}
}
func drawWheelLighting(ctx *canvas.Context, cx, cy float64) {
drawHighlightOval(ctx, cx-30, cy-50, 44, 18)
drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-5), 2, wheelBetaBevelColorIndex)
drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-8), 1, wheelBetaHighlightColorIndex)
func drawWheelLighting(img *image.Paletted, cx, cy int) {
outer := wheelBetaRadius * wheelBetaRadius
edgeStart := wheelBetaRadius - 13
edge := edgeStart * edgeStart
for y := cy - wheelBetaRadius; y <= cy+wheelBetaRadius; y++ {
for x := cx - wheelBetaRadius; x <= cx+wheelBetaRadius; x++ {
dx, dy := x-cx, y-cy
d2 := dx*dx + dy*dy
if d2 > outer || d2 < edge {
continue
}
if dx+dy > wheelBetaRadius/3 {
img.SetColorIndex(x, y, wheelBetaBevelColorIndex)
}
if dx+dy < -wheelBetaRadius {
img.SetColorIndex(x, y, wheelBetaHighlightColorIndex)
}
}
}
drawHighlightOval(img, cx-30, cy-50, 44, 18)
}
func drawHighlightOval(ctx *canvas.Context, cx, cy, rx, ry float64) {
ctx.SetFillColor(wheelBetaPalette[wheelBetaHighlightColorIndex])
ctx.DrawPath(cx, cy, canvas.Ellipse(rx, ry))
ctx.Fill()
func drawHighlightOval(img *image.Paletted, cx, cy, rx, ry int) {
limit := rx * rx * ry * ry
for y := cy - ry; y <= cy+ry; y++ {
for x := cx - rx; x <= cx+rx; x++ {
dx := x - cx
dy := y - cy
if dx*dx*ry*ry+dy*dy*rx*rx <= limit {
setWheelBetaPixel(img, x, y, wheelBetaHighlightColorIndex)
}
}
}
}
func drawWheelRim(ctx *canvas.Context, cx, cy float64) {
drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius)-1.5, 3, wheelBetaInkColorIndex)
drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-5), 1, wheelBetaBevelColorIndex)
drawCircleOutline(ctx, cx, cy, float64(wheelBetaRadius-8), 1, wheelBetaHighlightColorIndex)
func drawWheelRim(img *image.Paletted, cx, cy int) {
drawCircleOutline(img, cx, cy, wheelBetaRadius, 3, wheelBetaInkColorIndex)
drawCircleOutline(img, cx, cy, wheelBetaRadius-5, 1, wheelBetaBevelColorIndex)
drawCircleOutline(img, cx, cy, wheelBetaRadius-8, 1, wheelBetaHighlightColorIndex)
}
func drawCircleOutline(ctx *canvas.Context, cx, cy, radius, thickness float64, colorIndex byte) {
ctx.SetFillColor(canvas.Transparent)
ctx.SetStrokeColor(wheelBetaPalette[colorIndex])
ctx.SetStrokeWidth(thickness)
ctx.DrawPath(cx, cy, canvas.Circle(radius))
ctx.Stroke()
func drawCircleOutline(img *image.Paletted, cx, cy, radius, thickness int, colorIndex byte) {
outer := radius * radius
innerRadius := radius - thickness
inner := innerRadius * innerRadius
for y := cy - radius; y <= cy+radius; y++ {
for x := cx - radius; x <= cx+radius; x++ {
dx, dy := x-cx, y-cy
d2 := dx*dx + dy*dy
if d2 <= outer && d2 >= inner {
img.SetColorIndex(x, y, colorIndex)
}
}
}
}
func drawPointer(ctx *canvas.Context, tipX, cy float64, fillColorIndex byte) {
outer := &canvas.Path{}
outer.MoveTo(tipX+2, cy)
outer.LineTo(tipX+34, cy+24)
outer.LineTo(tipX+34, cy-24)
outer.Close()
drawWheelBetaPath(ctx, outer, wheelBetaInkColorIndex)
drawWheelBetaRect(ctx, tipX-1, cy-2, tipX+5, cy+2, wheelBetaInkColorIndex)
inner := &canvas.Path{}
inner.MoveTo(tipX+4, cy)
inner.LineTo(tipX+30, cy+10)
inner.LineTo(tipX+30, cy-10)
inner.Close()
drawWheelBetaPath(ctx, inner, fillColorIndex)
func drawPointer(img *image.Paletted, tipX, cy int, fillColorIndex byte) {
for xOffset := 0; xOffset < 34; xOffset++ {
half := xOffset / 2
x := tipX + xOffset
for y := cy - half; y <= cy+half; y++ {
setWheelBetaPixel(img, x, y, wheelBetaInkColorIndex)
}
}
for xOffset := 3; xOffset < 30; xOffset++ {
half := xOffset/2 - 2
if half < 0 {
continue
}
x := tipX + xOffset
for y := cy - half; y <= cy+half; y++ {
setWheelBetaPixel(img, x, y, fillColorIndex)
}
}
}
func drawWinnerCelebration(ctx *canvas.Context, step int) {
func drawWinnerCelebration(img *image.Paletted, step int) {
if step < 0 {
return
}
cx := float64(wheelBetaSize) / 2
cy := float64(wheelBetaSize) / 2
cx, cy := wheelBetaSize/2, wheelBetaSize/2
phase := step % wheelBetaCelebrateFrames
ringRadius := 34 + phase*5
if ringRadius < wheelBetaRadius-8 {
drawCircleOutline(ctx, cx, cy, float64(ringRadius), 1, wheelBetaSparkColorIndex)
drawCircleOutline(img, cx, cy, ringRadius, 1, wheelBetaSparkColorIndex)
}
for i := 0; i < 14; i++ {
angle := (float64(i)/14)*2*math.Pi + float64(phase)*0.31
inner := float64(wheelBetaRadius + 9 + phase%3)
outer := inner + 7 + float64(phase%4)
x1 := cx + math.Cos(angle)*inner
y1 := cy + math.Sin(angle)*inner
x2 := cx + math.Cos(angle)*outer
y2 := cy + math.Sin(angle)*outer
x1 := cx + int(math.Round(math.Cos(angle)*inner))
y1 := cy + int(math.Round(math.Sin(angle)*inner))
x2 := cx + int(math.Round(math.Cos(angle)*outer))
y2 := cy + int(math.Round(math.Sin(angle)*outer))
colorIndex := wheelBetaSliceColorIndexes[(i+phase)%len(wheelBetaSliceColorIndexes)]
if i%5 == 0 {
colorIndex = wheelBetaSparkColorIndex
}
drawWheelBetaLine(ctx, x1, y1, x2, y2, 2, colorIndex)
drawSpark(ctx, x2, y2, colorIndex)
drawPalettedLine(img, x1, y1, x2, y2, colorIndex)
drawSpark(img, x2, y2, colorIndex)
}
}
func drawStatusBand(ctx *canvas.Context, label, value string) {
drawWheelBetaRect(ctx, 31, 235, float64(wheelBetaSize-25), 286, wheelBetaShadowColorIndex)
drawWheelBetaRect(ctx, 28, 230, float64(wheelBetaSize-28), 282, wheelBetaPaperColorIndex)
drawWheelBetaLine(ctx, 28, 230, float64(wheelBetaSize-28), 230, 1, wheelBetaHighlightColorIndex)
drawWheelBetaLine(ctx, 28, 281, float64(wheelBetaSize-28), 281, 1, wheelBetaBevelColorIndex)
drawCenteredText(ctx, label, 240, wheelBetaInkColorIndex)
drawCenteredText(ctx, value, 260, wheelBetaInkColorIndex)
func drawStatusBand(img *image.Paletted, label, value string) {
for y := 235; y < 286; y++ {
for x := 31; x < wheelBetaSize-25; x++ {
img.SetColorIndex(x, y, wheelBetaShadowColorIndex)
}
}
for y := 230; y < 282; y++ {
for x := 28; x < wheelBetaSize-28; x++ {
img.SetColorIndex(x, y, wheelBetaPaperColorIndex)
}
}
for x := 28; x < wheelBetaSize-28; x++ {
img.SetColorIndex(x, 230, wheelBetaHighlightColorIndex)
img.SetColorIndex(x, 281, wheelBetaBevelColorIndex)
}
drawCenteredText(img, label, 250, wheelBetaInkColorIndex)
drawCenteredText(img, value, 270, wheelBetaInkColorIndex)
}
func drawCenteredText(ctx *canvas.Context, text string, topY float64, colorIndex byte) {
drawCenteredTextAt(ctx, text, float64(wheelBetaSize)/2, topY, colorIndex)
func drawCenteredText(img *image.Paletted, text string, baselineY int, colorIndex byte) {
drawCenteredTextAt(img, text, wheelBetaSize/2, baselineY, colorIndex)
}
func drawCenteredTextAt(ctx *canvas.Context, text string, centerX, topY float64, colorIndex byte) {
face := newWheelBetaTextFace(colorIndex)
ctx.DrawText(centerX, topY, canvas.NewTextLine(face, text, canvas.Center))
func drawCenteredTextAt(img *image.Paletted, text string, centerX, baselineY int, colorIndex byte) {
face := basicfont.Face7x13
width := font.MeasureString(face, text).Ceil()
x := centerX - width/2
drawer := font.Drawer{
Dst: img,
Src: image.NewUniform(wheelBetaPalette[colorIndex]),
Face: face,
Dot: fixed.P(x, baselineY),
}
drawer.DrawString(text)
}
func drawRotatedCenteredTextAt(ctx *canvas.Context, text string, centerX, centerY, angle float64, colorIndex byte) {
face := newWheelBetaTextFace(colorIndex)
topY := centerY - face.Metrics().LineHeight/2
ctx.Push()
ctx.RotateAbout(angle*180/math.Pi, centerX, centerY)
ctx.DrawText(centerX, topY, canvas.NewTextLine(face, text, canvas.Center))
ctx.Pop()
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 wheelBetaDisplayText(s string, limit int) string {
var b strings.Builder
count := 0
for _, r := range s {
for _, r := range norm.NFD.String(s) {
if unicode.Is(unicode.Mn, r) {
continue
}
if count >= limit {
break
}
r = wheelBetaASCIIRune(r)
if unicode.IsControl(r) {
b.WriteByte('?')
count++
continue
}
if r < 32 || r > 126 {
b.WriteByte('?')
count++
continue
}
b.WriteRune(r)
count++
}
@@ -266,35 +280,65 @@ func wheelBetaDisplayText(s string, limit int) string {
return out
}
func drawSpark(ctx *canvas.Context, cx, cy float64, colorIndex byte) {
drawWheelBetaLine(ctx, cx-2, cy, cx+2, cy, 1, colorIndex)
drawWheelBetaLine(ctx, cx, cy-2, cx, cy+2, 1, colorIndex)
func wheelBetaASCIIRune(r rune) rune {
switch r {
case 'đ':
return 'd'
case 'Đ':
return 'D'
default:
return r
}
}
func drawWheelBetaLine(ctx *canvas.Context, x0, y0, x1, y1, width float64, colorIndex byte) {
ctx.SetFillColor(canvas.Transparent)
ctx.SetStrokeColor(wheelBetaPalette[colorIndex])
ctx.SetStrokeWidth(width)
path := &canvas.Path{}
path.MoveTo(x0, y0)
path.LineTo(x1, y1)
ctx.DrawPath(0, 0, path)
ctx.Stroke()
func drawSpark(img *image.Paletted, cx, cy int, colorIndex byte) {
for y := cy - 1; y <= cy+1; y++ {
for x := cx - 1; x <= cx+1; x++ {
if x == cx || y == cy {
setWheelBetaPixel(img, x, y, colorIndex)
}
}
}
}
func drawWheelBetaRect(ctx *canvas.Context, x0, y0, x1, y1 float64, colorIndex byte) {
path := &canvas.Path{}
path.MoveTo(x0, y0)
path.LineTo(x1, y0)
path.LineTo(x1, y1)
path.LineTo(x0, y1)
path.Close()
drawWheelBetaPath(ctx, path, colorIndex)
func drawPalettedLine(img *image.Paletted, x0, y0, x1, y1 int, colorIndex byte) {
dx := absInt(x1 - x0)
sx := -1
if x0 < x1 {
sx = 1
}
dy := -absInt(y1 - y0)
sy := -1
if y0 < y1 {
sy = 1
}
err := dx + dy
for {
setWheelBetaPixel(img, x0, y0, colorIndex)
if x0 == x1 && y0 == y1 {
return
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
func drawWheelBetaPath(ctx *canvas.Context, path *canvas.Path, colorIndex byte) {
ctx.SetStrokeColor(canvas.Transparent)
ctx.SetFillColor(wheelBetaPalette[colorIndex])
ctx.DrawPath(0, 0, path)
ctx.Fill()
func setWheelBetaPixel(img *image.Paletted, x, y int, colorIndex byte) {
if image.Pt(x, y).In(img.Rect) {
img.SetColorIndex(x, y, colorIndex)
}
}
func absInt(v int) int {
if v < 0 {
return -v
}
return v
}