mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-07-28 08:20:20 +00:00
feat(misc): improve wheel beta spin animation
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"image"
|
||||
"image/gif"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -310,6 +311,49 @@ func TestWheelOfNamesBeta_CurrentOptionTracksPointer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWheelOfNamesBeta_RandomSpinProfileKeepsWinnerUnderPointer(t *testing.T) {
|
||||
rng := rand.New(rand.NewPCG(1, 2))
|
||||
for optionCount := 2; optionCount <= 10; optionCount++ {
|
||||
for winner := 0; winner < optionCount; winner++ {
|
||||
for spin := 0; spin < 20; spin++ {
|
||||
profile := newWheelBetaSpinProfile(optionCount, winner, rng)
|
||||
if got := currentWheelBetaIndex(optionCount, profile.finalRotation); got != winner {
|
||||
t.Fatalf("optionCount=%d winner=%d spin=%d final index = %d", optionCount, winner, spin, got)
|
||||
}
|
||||
if got := profile.rotationAt(1); math.Abs(got-profile.finalRotation) > 1e-9 {
|
||||
t.Fatalf("rotationAt(1) = %f, want final %f", got, profile.finalRotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWheelOfNamesBeta_SpinProfileVariesBetweenSpins(t *testing.T) {
|
||||
rng := rand.New(rand.NewPCG(10, 20))
|
||||
first := newWheelBetaSpinProfile(5, 2, rng)
|
||||
second := newWheelBetaSpinProfile(5, 2, rng)
|
||||
if first.startRotation == second.startRotation &&
|
||||
first.finalRotation == second.finalRotation &&
|
||||
first.accelEnd == second.accelEnd &&
|
||||
first.coastEnd == second.coastEnd &&
|
||||
first.wobblePhase == second.wobblePhase {
|
||||
t.Fatalf("spin profiles did not vary: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWheelOfNamesBeta_SpinProfileProgressIsMonotonic(t *testing.T) {
|
||||
rng := rand.New(rand.NewPCG(30, 40))
|
||||
profile := newWheelBetaSpinProfile(6, 4, rng)
|
||||
prev := -1.0
|
||||
for i := 0; i <= 100; i++ {
|
||||
progress := profile.progressAt(float64(i) / 100)
|
||||
if progress < prev {
|
||||
t.Fatalf("progress at step %d = %f, previous %f", i, progress, prev)
|
||||
}
|
||||
prev = progress
|
||||
}
|
||||
}
|
||||
|
||||
func TestWheelOfNamesBeta_PointerPointsIntoWheel(t *testing.T) {
|
||||
img := renderWheelBetaFrame([]string{"Alice", "Bob"}, 0, finalWheelRotation(2, 0), false)
|
||||
cx := wheelBetaSize / 2
|
||||
|
||||
@@ -47,17 +47,13 @@ func renderWheelOfNamesBetaGIF(options []string, winner int) ([]byte, error) {
|
||||
|
||||
frames := make([]*image.Paletted, 0, wheelBetaSpinFrames+wheelBetaHoldFrames)
|
||||
delays := make([]int, 0, wheelBetaSpinFrames+wheelBetaHoldFrames)
|
||||
finalRotation := finalWheelRotation(len(options), winner)
|
||||
startRotation := finalRotation - 8*2*math.Pi
|
||||
profile := newWheelBetaSpinProfile(len(options), winner, nil)
|
||||
for i := 0; i < wheelBetaSpinFrames; i++ {
|
||||
t := float64(i) / float64(wheelBetaSpinFrames-1)
|
||||
remaining := 1 - t
|
||||
progress := 1 - remaining*remaining*remaining
|
||||
rotation := startRotation + (finalRotation-startRotation)*progress
|
||||
frames = append(frames, renderWheelBetaFrame(options, winner, rotation, false))
|
||||
frames = append(frames, renderWheelBetaFrameWithStatus(options, winner, profile.rotationAt(t), false, profile.statusAt(t)))
|
||||
delays = append(delays, wheelBetaSpinDelay)
|
||||
}
|
||||
resultFrame := renderWheelBetaFrame(options, winner, finalRotation, true)
|
||||
resultFrame := renderWheelBetaFrame(options, winner, profile.finalRotation, true)
|
||||
for i := 0; i < wheelBetaHoldFrames; i++ {
|
||||
frames = append(frames, resultFrame)
|
||||
delays = append(delays, wheelBetaHoldDelay)
|
||||
@@ -81,6 +77,10 @@ func renderWheelOfNamesBetaGIF(options []string, winner int) ([]byte, error) {
|
||||
}
|
||||
|
||||
func renderWheelBetaFrame(options []string, winner int, rotation float64, reveal bool) *image.Paletted {
|
||||
return renderWheelBetaFrameWithStatus(options, winner, rotation, reveal, "")
|
||||
}
|
||||
|
||||
func renderWheelBetaFrameWithStatus(options []string, winner int, rotation float64, reveal bool, status string) *image.Paletted {
|
||||
rect := image.Rect(0, 0, wheelBetaSize, wheelBetaSize)
|
||||
img := image.NewPaletted(rect, wheelBetaPalette)
|
||||
draw.Draw(img, rect, image.NewUniform(wheelBetaPalette[0]), image.Point{}, draw.Src)
|
||||
@@ -101,12 +101,16 @@ func renderWheelBetaFrame(options []string, winner int, rotation float64, reveal
|
||||
}
|
||||
}
|
||||
|
||||
drawWheelRim(img, cx, cy)
|
||||
drawWheelSliceLabels(img, options, rotation)
|
||||
drawCircle(img, cx, cy, 24, 2)
|
||||
drawCircle(img, cx, cy, 18, 1)
|
||||
drawPointer(img, cx, cy-wheelBetaRadius)
|
||||
drawCenteredText(img, "WHEELOFNAMES BETA", cy+wheelBetaRadius+34, 1)
|
||||
label := "CURRENT"
|
||||
label := status
|
||||
if label == "" {
|
||||
label = "CURRENT"
|
||||
}
|
||||
value := asciiWheelText(options[currentWheelBetaIndex(len(options), rotation)], 28)
|
||||
if reveal {
|
||||
label = "RESULT"
|
||||
@@ -117,8 +121,12 @@ func renderWheelBetaFrame(options []string, winner int, rotation float64, reveal
|
||||
}
|
||||
|
||||
func finalWheelRotation(optionCount, winner int) float64 {
|
||||
return finalWheelRotationWithOffset(optionCount, winner, 0)
|
||||
}
|
||||
|
||||
func finalWheelRotationWithOffset(optionCount, winner int, sliceOffset float64) float64 {
|
||||
segment := 2 * math.Pi / float64(optionCount)
|
||||
return -math.Pi/2 - (float64(winner)+0.5)*segment
|
||||
return -math.Pi/2 - (float64(winner)+0.5+sliceOffset)*segment
|
||||
}
|
||||
|
||||
func currentWheelBetaIndex(optionCount int, rotation float64) int {
|
||||
|
||||
@@ -57,6 +57,26 @@ func drawCircle(img *image.Paletted, cx, cy, radius int, colorIndex byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func drawWheelRim(img *image.Paletted, cx, cy int) {
|
||||
drawCircleOutline(img, cx, cy, wheelBetaRadius, 3, 1)
|
||||
drawCircleOutline(img, cx, cy, wheelBetaRadius-6, 1, 2)
|
||||
}
|
||||
|
||||
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(img *image.Paletted, cx, tipY int) {
|
||||
for y := 0; y < 34; y++ {
|
||||
half := y / 2
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package misc
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
wheelBetaMinRevolutions = 7
|
||||
wheelBetaRandomRevolutionRange = 5
|
||||
wheelBetaMaxLandingOffset = 0.34
|
||||
)
|
||||
|
||||
type wheelBetaSpinProfile struct {
|
||||
startRotation float64
|
||||
finalRotation float64
|
||||
accelEnd float64
|
||||
coastEnd float64
|
||||
wobbleAmplitude float64
|
||||
wobbleCycles float64
|
||||
wobblePhase float64
|
||||
}
|
||||
|
||||
func newWheelBetaSpinProfile(optionCount, winner int, rng *rand.Rand) wheelBetaSpinProfile {
|
||||
segment := 2 * math.Pi / float64(optionCount)
|
||||
landingOffset := (wheelBetaRandFloat64(rng)*2 - 1) * wheelBetaMaxLandingOffset
|
||||
finalRotation := finalWheelRotationWithOffset(optionCount, winner, landingOffset)
|
||||
revolutions := wheelBetaMinRevolutions + wheelBetaRandIntN(rng, wheelBetaRandomRevolutionRange)
|
||||
accelEnd := 0.12 + wheelBetaRandFloat64(rng)*0.1
|
||||
coastEnd := accelEnd + 0.18 + wheelBetaRandFloat64(rng)*0.18
|
||||
if coastEnd > 0.58 {
|
||||
coastEnd = 0.58
|
||||
}
|
||||
return wheelBetaSpinProfile{
|
||||
startRotation: finalRotation - float64(revolutions)*2*math.Pi,
|
||||
finalRotation: finalRotation,
|
||||
accelEnd: accelEnd,
|
||||
coastEnd: coastEnd,
|
||||
wobbleAmplitude: math.Min(segment*0.075, 0.09) * (0.55 + wheelBetaRandFloat64(rng)*0.45),
|
||||
wobbleCycles: 3.5 + wheelBetaRandFloat64(rng)*2.5,
|
||||
wobblePhase: wheelBetaRandFloat64(rng) * 2 * math.Pi,
|
||||
}
|
||||
}
|
||||
|
||||
func (p wheelBetaSpinProfile) rotationAt(t float64) float64 {
|
||||
t = clampWheelBetaProgress(t)
|
||||
progress := p.progressAt(t)
|
||||
rotation := p.startRotation + (p.finalRotation-p.startRotation)*progress
|
||||
return rotation + p.wobbleAt(t)
|
||||
}
|
||||
|
||||
func (p wheelBetaSpinProfile) statusAt(t float64) string {
|
||||
switch {
|
||||
case t < p.accelEnd:
|
||||
return "BUILDING SPEED"
|
||||
case t < p.coastEnd:
|
||||
return "FULL SPIN"
|
||||
case t < 0.82:
|
||||
return "HEAVY SLOWDOWN"
|
||||
case t < 0.96:
|
||||
return "LOCKING IN"
|
||||
default:
|
||||
return "LAST TICK"
|
||||
}
|
||||
}
|
||||
|
||||
func (p wheelBetaSpinProfile) progressAt(t float64) float64 {
|
||||
t = clampWheelBetaProgress(t)
|
||||
accelEnd := p.accelEnd
|
||||
coastEnd := p.coastEnd
|
||||
if accelEnd <= 0 || coastEnd <= accelEnd || coastEnd >= 1 {
|
||||
return 1 - math.Pow(1-t, 3)
|
||||
}
|
||||
|
||||
accelArea := accelEnd * 0.5
|
||||
coastArea := coastEnd - accelEnd
|
||||
decelDuration := 1 - coastEnd
|
||||
decelArea := decelDuration * 0.25
|
||||
totalArea := accelArea + coastArea + decelArea
|
||||
|
||||
var distance float64
|
||||
switch {
|
||||
case t < accelEnd:
|
||||
u := t / accelEnd
|
||||
distance = accelEnd * (u*u*u - 0.5*u*u*u*u)
|
||||
case t < coastEnd:
|
||||
distance = accelArea + (t - accelEnd)
|
||||
default:
|
||||
u := (t - coastEnd) / decelDuration
|
||||
distance = accelArea + coastArea + decelDuration*(u-1.5*u*u+u*u*u-0.25*u*u*u*u)
|
||||
}
|
||||
return distance / totalArea
|
||||
}
|
||||
|
||||
func (p wheelBetaSpinProfile) wobbleAt(t float64) float64 {
|
||||
if t <= p.coastEnd || t >= 1 {
|
||||
return 0
|
||||
}
|
||||
u := (t - p.coastEnd) / (1 - p.coastEnd)
|
||||
envelope := math.Sin(u*math.Pi) * math.Pow(1-u, 1.4)
|
||||
return p.wobbleAmplitude * envelope * math.Sin(p.wobblePhase+u*p.wobbleCycles*2*math.Pi)
|
||||
}
|
||||
|
||||
func clampWheelBetaProgress(t float64) float64 {
|
||||
switch {
|
||||
case t < 0:
|
||||
return 0
|
||||
case t > 1:
|
||||
return 1
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func wheelBetaRandFloat64(rng *rand.Rand) float64 {
|
||||
if rng != nil {
|
||||
return rng.Float64()
|
||||
}
|
||||
return rand.Float64()
|
||||
}
|
||||
|
||||
func wheelBetaRandIntN(rng *rand.Rand, n int) int {
|
||||
if n <= 0 {
|
||||
return 0
|
||||
}
|
||||
if rng != nil {
|
||||
return rng.IntN(n)
|
||||
}
|
||||
return rand.IntN(n)
|
||||
}
|
||||
Reference in New Issue
Block a user