feat(misc): render wheel beta with canvas

This commit is contained in:
tiennm99 committed 2026-07-06 15:18:47 +07:00
1 parent 5e07ccc4f6
commit e7154d27f3
10 files changed
+734 -310

No files matched your search

+11 -35
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"image"
"image/color"
"image/gif"
"math"
"math/rand/v2"
@@ -13,7 +12,7 @@ import (
"time"
"github.com/go-telegram/bot/models"
"golang.org/x/image/font/sfnt"
"github.com/tdewolff/canvas"
"github.com/tiennm99/miti99bot/internal/modules"
"github.com/tiennm99/miti99bot/internal/storage"
@@ -427,25 +426,6 @@ func TestWheelOfNamesBeta_RotatesOptionLabelsWithSlices(t *testing.T) {
}
}
func TestWheelOfNamesBeta_RotatedLabelIgnoresFaintMaskPixels(t *testing.T) {
img := image.NewPaletted(image.Rect(0, 0, 7, 7), wheelBetaPalette)
mask := image.NewAlpha(image.Rect(0, 0, 3, 1))
mask.SetAlpha(0, 0, color.Alpha{A: wheelBetaSliceLabelAlphaThreshold - 1})
mask.SetAlpha(1, 0, color.Alpha{A: wheelBetaSliceLabelAlphaThreshold})
mask.SetAlpha(2, 0, color.Alpha{A: 255})
stampRotatedTextMask(img, mask, 3, 3, 0, wheelBetaInkColorIndex, wheelBetaSliceLabelAlphaThreshold)
if got := img.ColorIndexAt(2, 3); got == wheelBetaInkColorIndex {
t.Fatalf("faint mask pixel became ink color %d", got)
}
for _, x := range []int{3, 4} {
if got := img.ColorIndexAt(x, 3); got != wheelBetaInkColorIndex {
t.Fatalf("strong mask pixel at x=%d color = %d, want ink color %d", x, got, wheelBetaInkColorIndex)
}
}
}
func TestWheelOfNamesBeta_DisplayTextPreservesVietnamese(t *testing.T) {
input := "Tiếng Việt Đặng"
got := wheelBetaDisplayText(input, 32)
@@ -458,30 +438,26 @@ func TestWheelOfNamesBeta_DisplayTextPreservesVietnamese(t *testing.T) {
}
func TestWheelOfNamesBeta_FontUsesNormalWeight(t *testing.T) {
subfamily, err := wheelBetaTextFont.Name(nil, sfnt.NameIDSubfamily)
if err != nil {
t.Fatalf("font subfamily: %v", err)
if got := wheelBetaTextFont.Style().Weight(); got != canvas.FontRegular {
t.Fatalf("font weight = %v, want regular non-bold face", got)
}
normalized := strings.ToLower(subfamily)
if strings.Contains(normalized, "bold") {
t.Fatalf("font subfamily = %q, want normal non-bold face", subfamily)
}
if normalized != "book" && normalized != "regular" {
t.Fatalf("font subfamily = %q, want normal face such as Book or Regular", subfamily)
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
}
glyphIndex, err := wheelBetaTextFont.GlyphIndex(nil, r)
if err != nil {
t.Fatalf("GlyphIndex(%q): %v", r, err)
glyphs := face.Glyphs(string(r))
if len(glyphs) == 0 {
t.Fatalf("Glyphs(%q) is empty, want Vietnamese glyph support", r)
}
if glyphIndex == 0 {
t.Fatalf("GlyphIndex(%q) = 0, want Vietnamese glyph support", r)
if glyphs[0].ID == 0 {
t.Fatalf("Glyphs(%q)[0].ID = 0, want Vietnamese glyph support", r)
}
}
}
+1 -33
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"image"
"image/color"
"image/draw"
"image/gif"
"math"
)
@@ -104,38 +103,8 @@ 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"
@@ -145,8 +114,7 @@ func renderWheelBetaFrameWithCelebration(options []string, winner int, rotation
label = "RESULT"
value = wheelBetaDisplayText(options[winner], 28)
}
drawStatusBand(img, label, value)
return img
return palettizeWheelBetaFrame(renderWheelBetaCanvasImage(options, rotation, pointerColor, label, value, celebrateStep))
}
func finalWheelRotation(optionCount, winner int) float64 {
+177 -238
View File
@@ -3,61 +3,119 @@ package misc
import (
_ "embed"
"image"
"image/color"
"image/draw"
"math"
"strings"
"unicode"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
"github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/rasterizer"
)
const (
wheelBetaSliceLabelRadius = 64
wheelBetaSliceLabelAlphaThreshold = 96
wheelBetaFontSize = 10
wheelBetaFontDPI = 72
wheelBetaSliceLabelRadius = 64
wheelBetaFontSize = 10
)
//go:embed fonts/dejavu-sans.ttf
var wheelBetaTextFontBytes []byte
var wheelBetaTextFont = mustWheelBetaTextFont()
var (
wheelBetaTextFont = mustWheelBetaTextFont()
wheelBetaTextFontFamily = mustWheelBetaTextFontFamily()
)
func mustWheelBetaTextFont() *opentype.Font {
parsed, err := opentype.Parse(wheelBetaTextFontBytes)
func mustWheelBetaTextFont() *canvas.Font {
parsed, err := canvas.LoadFont(wheelBetaTextFontBytes, 0, canvas.FontRegular)
if err != nil {
panic(err)
}
return parsed
}
func newWheelBetaTextFace() font.Face {
face, err := opentype.NewFace(wheelBetaTextFont, &opentype.FaceOptions{
Size: wheelBetaFontSize,
DPI: wheelBetaFontDPI,
Hinting: font.HintingFull,
})
if err != nil {
func mustWheelBetaTextFontFamily() *canvas.FontFamily {
family := canvas.NewFontFamily("wheel-beta")
if err := family.LoadFont(wheelBetaTextFontBytes, 0, canvas.FontRegular); err != nil {
panic(err)
}
return face
return family
}
func drawWheelSliceLabels(img *image.Paletted, options []string, rotation float64) {
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) {
if len(options) == 0 {
return
}
cx, cy := wheelBetaSize/2, wheelBetaSize/2
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
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 + int(math.Round(math.Cos(angle)*float64(wheelBetaSliceLabelRadius)))
centerY := cy + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius)))
centerX := cx + math.Cos(angle)*float64(wheelBetaSliceLabelRadius)
centerY := cy + math.Sin(angle)*float64(wheelBetaSliceLabelRadius)
text := wheelBetaDisplayText(option, limit)
drawRotatedCenteredTextAt(img, text, centerX, centerY, angle, wheelBetaInkColorIndex, wheelBetaSliceLabelAlphaThreshold)
drawRotatedCenteredTextAt(ctx, text, centerX, centerY, angle, wheelBetaInkColorIndex)
}
}
@@ -74,216 +132,116 @@ func wheelBetaSliceLabelLimit(optionCount int) int {
}
}
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 drawCircle(ctx *canvas.Context, cx, cy, radius float64, colorIndex byte) {
ctx.SetFillColor(wheelBetaPalette[colorIndex])
ctx.DrawPath(cx, cy, canvas.Circle(radius))
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 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 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 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 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 drawHighlightOval(ctx *canvas.Context, cx, cy, rx, ry float64) {
ctx.SetFillColor(wheelBetaPalette[wheelBetaHighlightColorIndex])
ctx.DrawPath(cx, cy, canvas.Ellipse(rx, ry))
ctx.Fill()
}
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 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 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 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 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 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 drawWinnerCelebration(img *image.Paletted, step int) {
func drawWinnerCelebration(ctx *canvas.Context, step int) {
if step < 0 {
return
}
cx, cy := wheelBetaSize/2, wheelBetaSize/2
cx := float64(wheelBetaSize) / 2
cy := float64(wheelBetaSize) / 2
phase := step % wheelBetaCelebrateFrames
ringRadius := 34 + phase*5
if ringRadius < wheelBetaRadius-8 {
drawCircleOutline(img, cx, cy, ringRadius, 1, wheelBetaSparkColorIndex)
drawCircleOutline(ctx, cx, cy, float64(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 + 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))
x1 := cx + math.Cos(angle)*inner
y1 := cy + math.Sin(angle)*inner
x2 := cx + math.Cos(angle)*outer
y2 := cy + math.Sin(angle)*outer
colorIndex := wheelBetaSliceColorIndexes[(i+phase)%len(wheelBetaSliceColorIndexes)]
if i%5 == 0 {
colorIndex = wheelBetaSparkColorIndex
}
drawPalettedLine(img, x1, y1, x2, y2, colorIndex)
drawSpark(img, x2, y2, colorIndex)
drawWheelBetaLine(ctx, x1, y1, x2, y2, 2, colorIndex)
drawSpark(ctx, x2, y2, colorIndex)
}
}
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 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 drawCenteredText(img *image.Paletted, text string, baselineY int, colorIndex byte) {
drawCenteredTextAt(img, text, wheelBetaSize/2, baselineY, colorIndex)
func drawCenteredText(ctx *canvas.Context, text string, topY float64, colorIndex byte) {
drawCenteredTextAt(ctx, text, float64(wheelBetaSize)/2, topY, colorIndex)
}
func drawCenteredTextAt(img *image.Paletted, text string, centerX, baselineY int, colorIndex byte) {
face := newWheelBetaTextFace()
defer func() {
_ = face.Close()
}()
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 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 drawRotatedCenteredTextAt(img *image.Paletted, text string, centerX, centerY int, angle float64, colorIndex byte, alphaThreshold byte) {
face := newWheelBetaTextFace()
defer func() {
_ = face.Close()
}()
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)
stampRotatedTextMask(img, mask, centerX, centerY, angle, colorIndex, alphaThreshold)
}
func stampRotatedTextMask(img *image.Paletted, mask *image.Alpha, centerX, centerY int, angle float64, colorIndex byte, alphaThreshold byte) {
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 < alphaThreshold {
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 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 wheelBetaDisplayText(s string, limit int) string {
@@ -308,54 +266,35 @@ func wheelBetaDisplayText(s string, limit int) string {
return out
}
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 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 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 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 setWheelBetaPixel(img *image.Paletted, x, y int, colorIndex byte) {
if image.Pt(x, y).In(img.Rect) {
img.SetColorIndex(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 absInt(v int) int {
if v < 0 {
return -v
}
return v
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()
}