package splashmenu import ( "bytes" "cosmos/diego/groovy" _ "embed" "image" "image/color" _ "image/jpeg" _ "image/png" "log" "math" "math/rand" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" ) const ( magicRatio = 12 maxGlitchOffset = 25 minGlitchInterval = 60 maxGlitchTimeOffset = 15 //in seconds fadeTimer = 1.0 fadeOffset = 2.0 ) var ( //go:embed assets/bsoft.png mImage_png []byte imgDimensions = groovy.Area{Width: 827, Height: 628} //go:embed assets/title.png bImage_png []byte titlePosition = groovy.Area{Width: 580, Height: 490} imgToBlur *ebiten.Image bImage *ebiten.Image glitchTimer uint ) func init() { // Decode an image from the image file's byte slice. img, _, err := image.Decode(bytes.NewReader(mImage_png)) if err != nil { log.Fatal(err) } imgToBlur = ebiten.NewImageFromImage(img) // Decode an image from the image file's byte slice. img, _, err = image.Decode(bytes.NewReader(bImage_png)) if err != nil { log.Fatal(err) } bImage = ebiten.NewImageFromImage(img) glitchTimer = 15 } type Fader struct { Curtain *ebiten.Image counter int timer float32 offset float32 alpha float32 fps float32 } func NewFader(x, y int, t, offset float32) Fader { f := Fader{ Curtain: ebiten.NewImage(x, y), timer: t, counter: 0, alpha: 0.0, fps: 60, offset: offset, } f.Curtain.Fill(color.RGBA{0x00, 0x00, 0x00, 0xFF}) return f } func (f *Fader) Update() { f.counter++ f.alpha = (float32(f.counter) - (f.offset * f.fps)) / (f.timer * f.fps) if f.alpha < 0 { f.alpha = 0 } } func (f *Fader) Draw(img *ebiten.Image) { op := &ebiten.DrawImageOptions{} op.ColorScale.Scale(1, 1, 1, f.alpha) img.DrawImage(f.Curtain, op) } func (f *Fader) IsComplete() bool { return f.alpha > 1.0 } type Bsoft struct { Dimensions groovy.Area events map[groovy.SceneEvent]func() bgcolor color.RGBA increment int renderTarget *ebiten.Image mosaicRatio uint countDown uint curtain Fader } func NewBsoft() Bsoft { return Bsoft{ bgcolor: backgroundBaseColor, events: make(map[groovy.SceneEvent]func()), mosaicRatio: 16, increment: 0, } } func (b *Bsoft) Update() error { b.increment++ var keysPressed []ebiten.Key keysPressed = inpututil.AppendPressedKeys(keysPressed[:0]) for _, k := range keysPressed { switch k { case ebiten.KeyUp: b.mosaicRatio = b.mosaicRatio + 1 case ebiten.KeyDown: if b.mosaicRatio > 1 { b.mosaicRatio = b.mosaicRatio - 1 } case ebiten.KeyQ: if b.events[groovy.COMPLETED] != nil { b.events[groovy.COMPLETED]() } default: } } if b.curtain.IsComplete() { if b.events[groovy.COMPLETED] != nil { b.events[groovy.COMPLETED]() b.curtain.Clear() } } b.curtain.Update() return nil } func (f *Fader) Clear() { f.alpha = 0 //reset fader f.counter = 0 } func (b *Bsoft) Draw(screen *ebiten.Image) { b.DrawGlitchLogo(screen) b.DrawGlitchLogoText(screen) b.curtain.Draw(screen) } func (b *Bsoft) SetEventHandler(event groovy.SceneEvent, f func()) { b.events[event] = f } // sets sene dimensions func (b *Bsoft) SetDimensions(a groovy.Area) { b.Dimensions = a b.renderTarget = ebiten.NewImage(imgDimensions.Width, imgDimensions.Height) b.curtain = NewFader(a.Width, a.Height, fadeTimer, fadeOffset) } func (b *Bsoft) DrawGlitchLogo(screen *ebiten.Image) { //summation of sinusoids with different frequencies for 'step' response on pixel-glithcy-ness var sum float64 = 0 for i := 0; i < 10; i++ { w := math.Sin(float64(b.increment) / (math.Pi * float64(i+1))) sum = sum + w } ratio := sum + magicRatio // Shrink the image once. op := &ebiten.DrawImageOptions{} op.GeoM.Scale(1/ratio, 1/ratio) b.renderTarget.DrawImage(imgToBlur, op) // Enlarge the shrunk image. // The filter is the nearest filter, so the result will be mosaic. op = &ebiten.DrawImageOptions{} op.GeoM.Scale(ratio, ratio) op.GeoM.Translate(float64(b.Dimensions.Width)/2-float64(imgDimensions.Width)/2, float64(b.Dimensions.Height)/2-float64(imgDimensions.Height)/2-35) screen.DrawImage(b.renderTarget, op) } func (b *Bsoft) DrawGlitchLogoText(screen *ebiten.Image) { //glitchy twitch title behaviour op := &ebiten.DrawImageOptions{} var glitchX int = 0 var glitchY int = 0 X := minGlitchInterval Y := maxGlitchTimeOffset //glithiness active until countdown resolved, randomize the delta offset //creates temporary glitch effect on the logo text, this is achieved as follows: // 1. if the glitch frame counter is active, glitch offset is in effect and computed // 2. otherwise, we engage the glitch after X frames have passed with a random Y frame offset if b.countDown > 0 { glitchX = rand.Intn(maxGlitchOffset) * getRandomDirection() glitchY = rand.Intn(maxGlitchOffset) * getRandomDirection() b.countDown-- } else if b.increment%(X+rand.Intn(Y)) == 0 { b.countDown = glitchTimer //set glitch timer, in frames } op.GeoM.Translate(float64(titlePosition.Width+glitchX), float64(titlePosition.Height+glitchY)) screen.DrawImage(bImage, op) } // returns +1 or -1 as an integer func getRandomDirection() int { if rand.Intn(2) == 1 { return -1 } return 1 }