mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-07-23 22:21:22 +00:00
feat(misc): polish wheel beta animation
This commit is contained in:
@@ -285,9 +285,13 @@ func TestWheelOfNamesBeta_RenderGIFTiming(t *testing.T) {
|
||||
if equalPalettedFrames(decoded.Image[wheelBetaSpinFrames-1], decoded.Image[wheelBetaSpinFrames]) {
|
||||
t.Fatalf("first result frame matches last spin frame, want visible RESULT transition")
|
||||
}
|
||||
for i := wheelBetaSpinFrames + 1; i < len(decoded.Image); i++ {
|
||||
if !equalPalettedFrames(decoded.Image[wheelBetaSpinFrames], decoded.Image[i]) {
|
||||
t.Fatalf("result hold frame %d differs from first result frame", i)
|
||||
if equalPalettedFrames(decoded.Image[wheelBetaSpinFrames], decoded.Image[wheelBetaSpinFrames+1]) {
|
||||
t.Fatalf("first celebration frame matches second celebration frame, want visible result burst")
|
||||
}
|
||||
firstStableHoldFrame := wheelBetaSpinFrames + wheelBetaCelebrateFrames
|
||||
for i := firstStableHoldFrame + 1; i < len(decoded.Image); i++ {
|
||||
if !equalPalettedFrames(decoded.Image[firstStableHoldFrame], decoded.Image[i]) {
|
||||
t.Fatalf("stable result hold frame %d differs from frame %d", i, firstStableHoldFrame)
|
||||
}
|
||||
}
|
||||
if decoded.LoopCount != -1 {
|
||||
@@ -335,7 +339,7 @@ func TestWheelOfNamesBeta_SpinProfileVariesBetweenSpins(t *testing.T) {
|
||||
if first.startRotation == second.startRotation &&
|
||||
first.finalRotation == second.finalRotation &&
|
||||
first.accelEnd == second.accelEnd &&
|
||||
first.coastEnd == second.coastEnd &&
|
||||
first.decelSharpness == second.decelSharpness &&
|
||||
first.wobblePhase == second.wobblePhase {
|
||||
t.Fatalf("spin profiles did not vary: %+v", first)
|
||||
}
|
||||
@@ -364,6 +368,9 @@ func TestWheelOfNamesBeta_PointerPointsIntoWheel(t *testing.T) {
|
||||
if got := img.ColorIndexAt(cx+5, tipY-10); got != 1 {
|
||||
t.Fatalf("pointer shoulder color = %d, want 1", got)
|
||||
}
|
||||
if got := img.ColorIndexAt(cx, tipY-12); got != wheelBetaSliceColorIndexes[0] {
|
||||
t.Fatalf("pointer body color = %d, want current slice color %d", got, wheelBetaSliceColorIndexes[0])
|
||||
}
|
||||
if got := img.ColorIndexAt(cx+5, tipY); got == 1 {
|
||||
t.Fatalf("pointer tip is too wide at color index %d", got)
|
||||
}
|
||||
|
||||
@@ -11,15 +11,26 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
wheelBetaSize = 320
|
||||
wheelBetaRadius = 118
|
||||
wheelBetaSpinDuration = 7
|
||||
wheelBetaHoldDuration = 3
|
||||
wheelBetaSpinDelay = 20
|
||||
wheelBetaSpinFrames = wheelBetaSpinDuration * 100 / wheelBetaSpinDelay
|
||||
wheelBetaHoldFrames = wheelBetaHoldDuration * 100 / wheelBetaSpinDelay
|
||||
wheelBetaHoldDelay = wheelBetaSpinDelay
|
||||
wheelBetaDuration = wheelBetaSpinDuration + wheelBetaHoldDuration
|
||||
wheelBetaSize = 320
|
||||
wheelBetaRadius = 118
|
||||
wheelBetaSpinDuration = 7
|
||||
wheelBetaHoldDuration = 3
|
||||
wheelBetaSpinDelay = 20
|
||||
wheelBetaSpinFrames = wheelBetaSpinDuration * 100 / wheelBetaSpinDelay
|
||||
wheelBetaHoldFrames = wheelBetaHoldDuration * 100 / wheelBetaSpinDelay
|
||||
wheelBetaHoldDelay = wheelBetaSpinDelay
|
||||
wheelBetaDuration = wheelBetaSpinDuration + wheelBetaHoldDuration
|
||||
wheelBetaCelebrateFrames = 8
|
||||
)
|
||||
|
||||
const (
|
||||
wheelBetaBackgroundColorIndex byte = 0
|
||||
wheelBetaInkColorIndex byte = 1
|
||||
wheelBetaPaperColorIndex byte = 2
|
||||
wheelBetaShadowColorIndex byte = 10
|
||||
wheelBetaBevelColorIndex byte = 11
|
||||
wheelBetaHighlightColorIndex byte = 12
|
||||
wheelBetaSparkColorIndex byte = 13
|
||||
)
|
||||
|
||||
var wheelBetaPalette = color.Palette{
|
||||
@@ -33,6 +44,10 @@ var wheelBetaPalette = color.Palette{
|
||||
color.RGBA{R: 147, G: 103, B: 196, A: 255},
|
||||
color.RGBA{R: 52, G: 197, B: 197, A: 255},
|
||||
color.RGBA{R: 235, G: 117, B: 164, A: 255},
|
||||
color.RGBA{R: 204, G: 212, B: 224, A: 255},
|
||||
color.RGBA{R: 82, G: 94, B: 111, A: 255},
|
||||
color.RGBA{R: 255, G: 244, B: 206, A: 255},
|
||||
color.RGBA{R: 255, G: 218, B: 89, A: 255},
|
||||
}
|
||||
|
||||
var wheelBetaSliceColorIndexes = []byte{3, 4, 5, 6, 7, 8, 9}
|
||||
@@ -53,9 +68,12 @@ func renderWheelOfNamesBetaGIF(options []string, winner int) ([]byte, error) {
|
||||
frames = append(frames, renderWheelBetaFrameWithStatus(options, winner, profile.rotationAt(t), false, profile.statusAt(t)))
|
||||
delays = append(delays, wheelBetaSpinDelay)
|
||||
}
|
||||
resultFrame := renderWheelBetaFrame(options, winner, profile.finalRotation, true)
|
||||
for i := 0; i < wheelBetaHoldFrames; i++ {
|
||||
frames = append(frames, resultFrame)
|
||||
celebrateStep := i
|
||||
if celebrateStep >= wheelBetaCelebrateFrames {
|
||||
celebrateStep = -1
|
||||
}
|
||||
frames = append(frames, renderWheelBetaFrameWithCelebration(options, winner, profile.finalRotation, true, "", celebrateStep))
|
||||
delays = append(delays, wheelBetaHoldDelay)
|
||||
}
|
||||
|
||||
@@ -81,11 +99,17 @@ func renderWheelBetaFrame(options []string, winner int, rotation float64, reveal
|
||||
}
|
||||
|
||||
func renderWheelBetaFrameWithStatus(options []string, winner int, rotation float64, reveal bool, status string) *image.Paletted {
|
||||
return renderWheelBetaFrameWithCelebration(options, winner, rotation, reveal, status, -1)
|
||||
}
|
||||
|
||||
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[0]), image.Point{}, draw.Src)
|
||||
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++ {
|
||||
@@ -101,17 +125,21 @@ func renderWheelBetaFrameWithStatus(options []string, winner int, rotation float
|
||||
}
|
||||
}
|
||||
|
||||
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, 2)
|
||||
drawCircle(img, cx, cy, 18, 1)
|
||||
drawPointer(img, cx, cy-wheelBetaRadius)
|
||||
drawCenteredText(img, "WHEELOFNAMES BETA", cy+wheelBetaRadius+34, 1)
|
||||
drawCircle(img, cx, cy, 24, wheelBetaPaperColorIndex)
|
||||
drawCircle(img, cx, cy, 18, wheelBetaInkColorIndex)
|
||||
drawPointer(img, cx, cy-wheelBetaRadius, pointerColor)
|
||||
drawCenteredText(img, "WHEELOFNAMES BETA", cy+wheelBetaRadius+34, wheelBetaInkColorIndex)
|
||||
label := status
|
||||
if label == "" {
|
||||
label = "CURRENT"
|
||||
}
|
||||
value := asciiWheelText(options[currentWheelBetaIndex(len(options), rotation)], 28)
|
||||
value := asciiWheelText(options[currentIndex], 28)
|
||||
if reveal {
|
||||
label = "RESULT"
|
||||
value = asciiWheelText(options[winner], 28)
|
||||
|
||||
@@ -57,9 +57,62 @@ func drawCircle(img *image.Paletted, cx, cy, radius int, colorIndex byte) {
|
||||
}
|
||||
}
|
||||
|
||||
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(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(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(img *image.Paletted, cx, cy int) {
|
||||
drawCircleOutline(img, cx, cy, wheelBetaRadius, 3, 1)
|
||||
drawCircleOutline(img, cx, cy, wheelBetaRadius-6, 1, 2)
|
||||
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(img *image.Paletted, cx, cy, radius, thickness int, colorIndex byte) {
|
||||
@@ -77,24 +130,72 @@ func drawCircleOutline(img *image.Paletted, cx, cy, radius, thickness int, color
|
||||
}
|
||||
}
|
||||
|
||||
func drawPointer(img *image.Paletted, cx, tipY int) {
|
||||
func drawPointer(img *image.Paletted, cx, tipY int, fillColorIndex byte) {
|
||||
for y := 0; y < 34; y++ {
|
||||
half := y / 2
|
||||
rowY := tipY - y
|
||||
for x := cx - half; x <= cx+half; x++ {
|
||||
img.SetColorIndex(x, rowY, 1)
|
||||
setWheelBetaPixel(img, x, rowY, wheelBetaInkColorIndex)
|
||||
}
|
||||
}
|
||||
for y := 3; y < 30; y++ {
|
||||
half := y/2 - 2
|
||||
if half < 0 {
|
||||
continue
|
||||
}
|
||||
rowY := tipY - y
|
||||
for x := cx - half; x <= cx+half; x++ {
|
||||
setWheelBetaPixel(img, x, rowY, fillColorIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func drawWinnerCelebration(img *image.Paletted, step int) {
|
||||
if step < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cx, cy := wheelBetaSize/2, wheelBetaSize/2
|
||||
phase := step % wheelBetaCelebrateFrames
|
||||
ringRadius := 34 + phase*5
|
||||
if ringRadius < wheelBetaRadius-8 {
|
||||
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 + 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
|
||||
}
|
||||
drawPalettedLine(img, x1, y1, x2, y2, colorIndex)
|
||||
drawSpark(img, x2, y2, colorIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func drawStatusBand(img *image.Paletted, label, value string) {
|
||||
for y := 230; y < 282; y++ {
|
||||
for x := 28; x < wheelBetaSize-28; x++ {
|
||||
img.SetColorIndex(x, y, 2)
|
||||
for y := 235; y < 286; y++ {
|
||||
for x := 31; x < wheelBetaSize-25; x++ {
|
||||
img.SetColorIndex(x, y, wheelBetaShadowColorIndex)
|
||||
}
|
||||
}
|
||||
drawCenteredText(img, label, 250, 1)
|
||||
drawCenteredText(img, value, 270, 1)
|
||||
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(img *image.Paletted, text string, baselineY int, colorIndex byte) {
|
||||
@@ -132,3 +233,55 @@ func asciiWheelText(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 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 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
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ type wheelBetaSpinProfile struct {
|
||||
startRotation float64
|
||||
finalRotation float64
|
||||
accelEnd float64
|
||||
coastEnd float64
|
||||
decelSharpness float64
|
||||
wobbleAmplitude float64
|
||||
wobbleCycles float64
|
||||
wobblePhase float64
|
||||
@@ -26,16 +26,12 @@ func newWheelBetaSpinProfile(optionCount, winner int, rng *rand.Rand) wheelBetaS
|
||||
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
|
||||
}
|
||||
accelEnd := 0.20 + wheelBetaRandFloat64(rng)*0.1
|
||||
return wheelBetaSpinProfile{
|
||||
startRotation: finalRotation - float64(revolutions)*2*math.Pi,
|
||||
finalRotation: finalRotation,
|
||||
accelEnd: accelEnd,
|
||||
coastEnd: coastEnd,
|
||||
decelSharpness: 3.6 + wheelBetaRandFloat64(rng)*1.8,
|
||||
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,
|
||||
@@ -53,10 +49,8 @@ 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"
|
||||
return "DECELERATING"
|
||||
case t < 0.96:
|
||||
return "LOCKING IN"
|
||||
default:
|
||||
@@ -66,38 +60,36 @@ func (p wheelBetaSpinProfile) statusAt(t float64) string {
|
||||
|
||||
func (p wheelBetaSpinProfile) progressAt(t float64) float64 {
|
||||
t = clampWheelBetaProgress(t)
|
||||
if t == 0 || t == 1 {
|
||||
return t
|
||||
}
|
||||
|
||||
accelEnd := p.accelEnd
|
||||
coastEnd := p.coastEnd
|
||||
if accelEnd <= 0 || coastEnd <= accelEnd || coastEnd >= 1 {
|
||||
if accelEnd <= 0 || accelEnd >= 1 {
|
||||
remaining := 1 - t
|
||||
return 1 - remaining*remaining*remaining
|
||||
}
|
||||
|
||||
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:
|
||||
accelDistance := accelEnd * 0.72
|
||||
if 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 accelDistance * u * u
|
||||
}
|
||||
return distance / totalArea
|
||||
|
||||
sharpness := p.decelSharpness
|
||||
if sharpness <= 0 {
|
||||
sharpness = 4
|
||||
}
|
||||
u := (t - accelEnd) / (1 - accelEnd)
|
||||
decelProgress := (1 - math.Exp(-sharpness*u)) / (1 - math.Exp(-sharpness))
|
||||
return accelDistance + (1-accelDistance)*decelProgress
|
||||
}
|
||||
|
||||
func (p wheelBetaSpinProfile) wobbleAt(t float64) float64 {
|
||||
if t <= p.coastEnd || t >= 1 {
|
||||
if t <= p.accelEnd || t >= 1 {
|
||||
return 0
|
||||
}
|
||||
u := (t - p.coastEnd) / (1 - p.coastEnd)
|
||||
u := (t - p.accelEnd) / (1 - p.accelEnd)
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user