diff --git a/examples/splashmenu/scenes/controller.go b/examples/splashmenu/scenes/controller.go new file mode 100644 index 0000000..4c00056 --- /dev/null +++ b/examples/splashmenu/scenes/controller.go @@ -0,0 +1,563 @@ +package splashmenu + +import ( + "bytes" + "cosmos/diego/groovy" + splashmenu "cosmos/diego/groovy/examples/splashmenu/fonts" + "fmt" + "image" + "image/color" + "log" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text" + "github.com/hajimehoshi/ebiten/v2/vector" + + _ "embed" +) + +const ( + FORCE_GRAVITY = 0 + FORCE_ATTACK = 1 + FORCE_ENVIRONMENT = 2 + MAX_FLOOR_POS = 720 - 50 + HERO_WIDTH = 20 + HERO_HEIGHT = HERO_WIDTH + + //initial thrust + MAX_JUMP_HEIGHT = -150 //pixels + TIME_TO_APEX = 20.0 // in frame counts (60fps means this value is 1/60 seconds) + JUMP_DURATION = TIME_TO_APEX * 2.0 + GRAVITY_VECTOR = -2.0 * MAX_JUMP_HEIGHT / (TIME_TO_APEX * TIME_TO_APEX) + INITIAL_JUMP_VELOCITY = -GRAVITY_VECTOR * TIME_TO_APEX + + //weighted drop + TIME_TO_APEX2 = 17.0 // in frame counts (60fps means this value is 1/60 seconds) + GRAVITY_VECTOR2 = -2.0 * MAX_JUMP_HEIGHT / (TIME_TO_APEX2 * TIME_TO_APEX2) + INITIAL_DROP_VELOCITY = -GRAVITY_VECTOR2 * TIME_TO_APEX2 + + //run animation ramp up + TOP_SPEED = 20 + TIME_TO_TOP_SPEED = 6 //in frames + VELOCITY_VECTOR = 2 * TOP_SPEED / (TIME_TO_TOP_SPEED * TIME_TO_TOP_SPEED) + + //decel animation ramp down + TIME_TO_STOP = 5 //in frames + + //attacking stats + MAX_HERO_ATTACK_DURATION = 22 //in frames + TOTAL_ATTACK_ANIMATION_IMAGES = 8 + FRAMES_PER_ATTACK_SLIDE = MAX_HERO_ATTACK_DURATION / TOTAL_ATTACK_ANIMATION_IMAGES + + SENSITIVITY = 0.15 +) + +var ( + whiteImage = ebiten.NewImage(3, 3) + whiteSubImage = whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) + + //go:embed assets/slash.png + slashImg_png []byte + //go:embed assets/slashright.png + slashRightImg_png []byte + + //go:embed assets/forest.png + forestImg_png []byte + + slashImgHeight = 60 + slashImgCellsWidth = 72 + //slashImgCells = 8 + //slashImgDimensions = groovy.Area{Width: 576, Height: slashImgHeight} + slashImage = ebiten.NewImage(576, slashImgHeight) + slashRightImage = ebiten.NewImage(576, slashImgHeight) + forestImage = ebiten.NewImage(1280, 720) + //slashRenderTarget = ebiten.NewImage(slashImgCellsWidth, slashImgHeight) + + slowmo = false +) + +func init() { + // Decode an image from the image file's byte slice. + img, _, err := image.Decode(bytes.NewReader(slashImg_png)) + if err != nil { + log.Fatal(err) + } + slashImage = ebiten.NewImageFromImage(img) + + img, _, err = image.Decode(bytes.NewReader(slashRightImg_png)) + if err != nil { + log.Fatal(err) + } + slashRightImage = ebiten.NewImageFromImage(img) + + img, _, err = image.Decode(bytes.NewReader(forestImg_png)) + if err != nil { + log.Fatal(err) + } + forestImage = ebiten.NewImageFromImage(img) +} + +type vec struct { + x float32 + y float32 +} + +type impulse struct { + f vec + timeleft int +} + +type forceType uint32 + +type MyChar struct { + posX int16 + posY int16 + mass float32 //g + + //acceleration + aX float32 + aY float32 + jumptime int + jumping bool + leftRamptime int + runningLeft bool + + rightRamptime int + runningRight bool + runningStopFrame int + + attacking bool + attacktime int + + facingLeft bool +} + +type force struct { + vector vec + ftype forceType +} + +func (v *vec) Size() float32 { + return float32(math.Sqrt(float64(v.x*v.x + v.y*v.y))) + +} + +type SplashPad struct { + increment int + Dimensions groovy.Area + events map[groovy.SceneEvent]func() + gamepadIDs map[ebiten.GamepadID]struct{} + + globalForces []force + + hero MyChar + heroImpulse impulse + jumping bool + jumptimer int +} + +func NewSplashPad() SplashPad { + h := MyChar{ + posX: 640, + posY: MAX_FLOOR_POS, + mass: 80000, + aX: 0, + aY: 0, + jumptime: 0, + jumping: false, + leftRamptime: 0, + runningLeft: false, + rightRamptime: 0, + runningRight: false, + runningStopFrame: 0, + attacking: false, + attacktime: 0, + facingLeft: false, + } + + s := SplashPad{ + increment: 0, + events: make(map[groovy.SceneEvent]func()), + gamepadIDs: make(map[ebiten.GamepadID]struct{}), + hero: h, + jumping: false, + jumptimer: 0, + } + + gravity := &force{} + gravity.vector = vec{x: 0, y: 9.8} + gravity.ftype = FORCE_GRAVITY + s.globalForces = append(s.globalForces, *gravity) + + s.heroImpulse = impulse{f: vec{x: 0, y: 0}, timeleft: 0} + + whiteImage.Fill(color.White) + log.Println("hero.PosY, prev.impulse.Y, impulse.Y, inc, netFy, timer") + + return s +} + +func (s *SplashPad) Draw(screen *ebiten.Image) { + + s.DrawBackground(screen) + s.DrawController(screen) + s.DrawHero(screen) + + if slowmo && s.increment/60%2 == 0 { + text.Draw(screen, " slow mo ", splashmenu.SplashFont.BigTitle, s.Dimensions.Width/2-140, 80, color.White) + } + +} + +func (s *SplashPad) Update() error { + + var keys []ebiten.Key + keys = inpututil.AppendJustPressedKeys(keys[:0]) + for _, k := range keys { + if k == ebiten.Key9 { + slowmo = !slowmo + } + } + + s.increment++ + keys = inpututil.AppendJustPressedKeys(keys[:0]) + for _, k := range keys { + if k == ebiten.KeyB { + s.hero.posX = int16(s.Dimensions.Width) / 2 + s.hero.posY = MAX_FLOOR_POS + } + } + + s.doThePhysics() + + if s.hero.jumping && s.hero.jumptime < JUMP_DURATION { + s.hero.jumptime++ + } else if s.hero.posY >= MAX_FLOOR_POS { + s.hero.posY = MAX_FLOOR_POS + s.heroImpulse.f.y = 0.0 + s.hero.jumptime = 0 + s.hero.jumping = false + } + return nil +} + +func (s *SplashPad) SetEventHandler(event groovy.SceneEvent, f func()) { + s.events[event] = f +} + +func (s *SplashPad) SetDimensions(a groovy.Area) { + s.Dimensions = a +} + +func (s *SplashPad) DrawController(screen *ebiten.Image) { + + const radInner = 20 + const radOuter = 25 + const maxOffset = radOuter - radInner + const padding = 20 + + //leftOriginX := float32(s.Dimensions.Width/2.0) - 1.75*radOuter + //leftOriginY := float32(s.Dimensions.Width / 2.0) + + leftOriginX := float32(s.Dimensions.Width - 3*(2*radOuter) - padding) + leftOriginY := float32((2 * radOuter) - padding) + + leftXOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisLeftStickHorizontal) * maxOffset) + leftYOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisLeftStickVertical) * maxOffset) + + vector.DrawFilledCircle(screen, leftOriginX, leftOriginY, radOuter, color.White, true) + vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner, color.Black, true) + vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner-5, color.White, true) + vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner-8, color.RGBA{0xff, 0x5c, 0x5c, 0xff}, true) + + //rightOriginX := float32(s.Dimensions.Width/2.0) + 1.75*radOuter + //rightOriginY := float32(s.Dimensions.Width / 2.0) + + rightOriginX := float32(s.Dimensions.Width - 2*(2*radOuter)) + rightOriginY := float32((2 * radOuter) - padding) + + rightXOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) * maxOffset) + rightYOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical) * maxOffset) + + vector.DrawFilledCircle(screen, rightOriginX, rightOriginY, radOuter, color.White, true) + vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner, color.Black, true) + vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner-5, color.White, true) + vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner-8, color.RGBA{0xff, 0x5c, 0x5c, 0xff}, true) + + gpInput := ebiten.GamepadAxisValue(0, 0) + text.Draw(screen, fmt.Sprintf("%1.02f", gpInput), splashmenu.SplashFont.Menu, 20, 20, color.White) +} + +func (s *SplashPad) DrawBackground(screen *ebiten.Image) { + screen.DrawImage(forestImage, nil) +} + +func (s *SplashPad) DrawHero(screen *ebiten.Image) { + + var path vector.Path + + //set up origin and boundaries + x0 := float32(s.hero.posX) + y0 := float32(s.hero.posY) + + xd := float32(HERO_WIDTH / 2.0) + yd := float32(HERO_WIDTH / 2.0) + + //connect the dots + path.MoveTo(x0-xd, y0-yd) + path.LineTo(x0+xd, y0-yd) + path.LineTo(x0+xd, y0+yd) + path.LineTo(x0-xd, y0+yd) + path.LineTo(x0-xd, y0+yd) + path.Close() + + //fill time + var vs []ebiten.Vertex + var is []uint16 + + vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) + + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = float32(0xff) / 0xFF + vs[i].ColorG = float32(0x00) / 0xFF + vs[i].ColorB = float32(0x00) / 0xFF + vs[i].ColorA = 1 + } + + //op := &ebiten.DrawTrianglesOptions{} + screen.DrawTriangles(vs, is, whiteSubImage, nil) + op := &ebiten.DrawImageOptions{} + //handle attacking animation + if s.hero.attacking { + + i := (s.hero.attacktime / FRAMES_PER_ATTACK_SLIDE) + + if s.hero.facingLeft { + op.GeoM.Translate(float64(s.hero.posX-int16(slashImgCellsWidth)/2-13), float64(s.hero.posY-HERO_HEIGHT/2-5)) + dx := i * slashImgCellsWidth + screen.DrawImage(slashImage.SubImage(image.Rect(dx, 0, dx+slashImgCellsWidth, slashImgHeight)).(*ebiten.Image), op) + } else { + op.GeoM.Translate(float64(s.hero.posX-24), float64(s.hero.posY-HERO_HEIGHT/2-5)) + dx := slashImgCellsWidth*TOTAL_ATTACK_ANIMATION_IMAGES - i*slashImgCellsWidth + screen.DrawImage(slashRightImage.SubImage(image.Rect(dx, 0, dx+slashImgCellsWidth, slashImgHeight)).(*ebiten.Image), op) + } + + } + + if s.hero.jumping { + text.Draw(screen, "hup!", splashmenu.SplashFont.Splash, int(s.hero.posX), int(s.hero.posY)-HERO_HEIGHT, color.White) + } + +} + +func (s *SplashPad) doThePhysics() { + var netFx float32 = 0 + var netFy float32 = 0 + + for _, i := range s.globalForces { + netFx = netFx + i.vector.x + netFy = netFy + i.vector.y + } + + //HANDLE JUMPING + s.handleJumping() + + //HANDLE LEFT/RIGHT MOVEMENT + s.handleLeftMovement() + s.handleRightMovement() + + //HANDLE ATTACK + s.handleAttack() + + //if s.hero.ramptime < TIME_TO_STOP { + // s.hero.posX = s.hero.posX - (VELOCITY_VECTOR * s.hero.ramptime) + //} else if s.hero.ramptime >= TIME_TO_TOP_SPEED { + // s.hero.posX = s.hero.posX - TOP_SPEED + //} + // + //HANDLE FLOOR COLLISION CHECK + if s.hero.posY > MAX_FLOOR_POS { + s.hero.posY = MAX_FLOOR_POS + } +} + +func (s *SplashPad) handleAttack() { + maxButton := ebiten.GamepadButton(ebiten.GamepadButtonCount(0)) + for b := ebiten.GamepadButton(0); b < maxButton; b++ { + if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton0) { + s.hero.attacking = true + } + } + + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + s.hero.attacking = true + } + + if s.hero.attacking { + s.hero.attacktime++ + + if s.hero.attacktime < MAX_HERO_ATTACK_DURATION { + + } else { + s.hero.attacking = false + s.hero.attacktime = 0 + } + } + +} + +func (s *SplashPad) handleJumping() { + maxButton := ebiten.GamepadButton(ebiten.GamepadButtonCount(0)) + for b := ebiten.GamepadButton(0); b < maxButton; b++ { + if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton1) { + if !s.hero.jumping { + s.hero.jumptime = 0 + s.hero.jumping = true + } + } + } + + if inpututil.IsKeyJustPressed(ebiten.KeySpace) { + if !s.hero.jumping { + s.hero.jumptime = 0 + s.hero.jumping = true + } + } + + if s.hero.jumping { + //asymmetric jump to simulate additional character weight + if s.hero.jumptime < TIME_TO_APEX { + t2 := float32((s.hero.jumptime * s.hero.jumptime)) + s.hero.posY = int16(0.5*GRAVITY_VECTOR*t2 + INITIAL_JUMP_VELOCITY*float32(s.hero.jumptime) + MAX_FLOOR_POS) + } else { + t2 := float32((s.hero.jumptime * s.hero.jumptime)) + s.hero.posY = int16(0.5*GRAVITY_VECTOR2*t2 + INITIAL_DROP_VELOCITY*float32(s.hero.jumptime) + MAX_FLOOR_POS) + + } + } +} + +func (s *SplashPad) handleLeftMovement() { + + var keys []ebiten.Key + keys = inpututil.AppendPressedKeys(keys[:0]) + for _, pressed := range keys { + if pressed == ebiten.KeyA { + s.hero.leftRamptime++ + s.hero.runningLeft = true + log.Println(" a engaged") + s.hero.facingLeft = true + } + } + + keys = inpututil.AppendJustReleasedKeys(keys[:0]) + for _, pressed := range keys { + if pressed == ebiten.KeyA { + s.hero.runningLeft = false + s.hero.runningStopFrame = s.increment + log.Println(" a released") + } + } + + if ebiten.GamepadAxisValue(0, 0) <= -SENSITIVITY { + s.hero.leftRamptime++ + s.hero.runningLeft = true + log.Println(" left gamepad engaged") + s.hero.facingLeft = true + } + + if -SENSITIVITY < ebiten.GamepadAxisValue(0, 0) && ebiten.GamepadAxisValue(0, 0) < SENSITIVITY { + s.hero.runningLeft = false + if s.hero.runningStopFrame > 0 { + s.hero.runningStopFrame = s.increment + log.Println(" left gamepad released") + } + + } + + //if running, need to update their position + var offset = 0 + if s.hero.runningLeft { + + if s.hero.leftRamptime < TIME_TO_TOP_SPEED { + offset = s.hero.leftRamptime * s.hero.leftRamptime + } else { + offset = TOP_SPEED + } + } + + ////we've let go, time to decel + //if !s.hero.runningLeft && s.hero.leftRamptime < { + // s.hero.posX = s.hero.posX + //} + + if s.hero.runningStopFrame > 0 { + if s.increment-s.hero.runningStopFrame < TIME_TO_STOP { + scaler := float32(TOP_SPEED) / float32(TIME_TO_STOP*TIME_TO_STOP) + deltaT := float32(s.increment - s.hero.runningStopFrame) + + offset = TOP_SPEED - int(scaler*deltaT) + offset = max(0, offset) //should not be less than zero, otherwise player would move wrong way ?? + log.Printf(" stopping (%d,%d)...", s.hero.posX, s.hero.posY) + } else { + s.hero.runningStopFrame = 0 + s.hero.leftRamptime = 0 + } + } + + s.hero.posX = s.hero.posX - int16(offset) + +} + +func (s *SplashPad) handleRightMovement() { + var keys []ebiten.Key + keys = inpututil.AppendPressedKeys(keys[:0]) + for _, pressed := range keys { + if pressed == ebiten.KeyD { + s.hero.rightRamptime++ + s.hero.runningRight = true + log.Println(" d engaged") + s.hero.facingLeft = false + } + } + + keys = inpututil.AppendJustReleasedKeys(keys[:0]) + for _, pressed := range keys { + if pressed == ebiten.KeyD { + s.hero.runningRight = false + log.Println(" d released") + } + } + + if ebiten.GamepadAxisValue(0, 0) >= SENSITIVITY { + s.hero.rightRamptime++ + s.hero.runningRight = true + log.Println(" right gamepad engaged") + s.hero.facingLeft = false + } + + if -SENSITIVITY < ebiten.GamepadAxisValue(0, 0) && ebiten.GamepadAxisValue(0, 0) < SENSITIVITY { + s.hero.runningRight = false + if s.hero.runningStopFrame > 0 { + s.hero.runningStopFrame = s.increment + log.Println(" right gamepad released") + } + + } + + //if running, need to update their position + var offset = 0 + if s.hero.runningRight { + + if s.hero.rightRamptime < TIME_TO_TOP_SPEED { + offset = s.hero.rightRamptime * s.hero.rightRamptime + } else { + offset = TOP_SPEED + } + s.hero.posX = s.hero.posX + int16(offset) + } +} diff --git a/examples/splashmenu/scenes/noise.go b/examples/splashmenu/scenes/noise.go new file mode 100644 index 0000000..2ec1177 --- /dev/null +++ b/examples/splashmenu/scenes/noise.go @@ -0,0 +1,157 @@ +package splashmenu + +import ( + "cosmos/diego/groovy" + splashmenu "cosmos/diego/groovy/examples/splashmenu/fonts" + "fmt" + "image" + "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text" +) + +var ( +// theRand = &randy{12345678, 4185243, 776511, 45411} +) + +const ( + scaleRatio = 12.0 + maxMagWidth = 100 +) + +//type randy struct { +// x, y, z, w uint32 +//} + +type Noisy struct { + increment int + Dimensions groovy.Area + magnifier groovy.Area + sX int + sY int + events map[groovy.SceneEvent]func() + pixImage *image.RGBA + pixSubImage *image.RGBA + subTarget *ebiten.Image + renderTarget *ebiten.Image + fader *ebiten.Image + //alpha float32 +} + +func NewNoisy() Noisy { + return Noisy{ + increment: 0, + events: make(map[groovy.SceneEvent]func()), + magnifier: groovy.Area{Width: maxMagWidth, Height: maxMagWidth}, + } +} + +func (n *Noisy) Update() error { + n.increment++ + + //n.alpha = float32(n.increment) / (60.0 * 5.0) //60fps * 5 seconds = 300frames + //fmt.Printf("%f, ", n.alpha) + + n.UpdateNoise() + return nil +} + +func (n *Noisy) Draw(screen *ebiten.Image) { + n.subTarget.WritePixels(n.pixSubImage.Pix) + + screen.WritePixels(n.pixImage.Pix) + //screen.WritePixels(n.pixSubImage.Pix) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Scale(1/scaleRatio, 1/scaleRatio) + n.renderTarget.DrawImage(n.subTarget, op) + op = &ebiten.DrawImageOptions{} + op.GeoM.Scale(scaleRatio, scaleRatio) + op.GeoM.Translate(float64(n.sX)-maxMagWidth/2, float64(n.sY)-maxMagWidth/2) + + screen.DrawImage(n.renderTarget, op) + + //screen.DrawImage(n.subTarget, nil) + + text.Draw(screen, fmt.Sprintf("%04d, ", n.sX), splashmenu.SplashFont.Menu, n.Dimensions.Width/2-58, n.Dimensions.Height/2+18, color.White) + text.Draw(screen, fmt.Sprintf("%04d ", n.sY), splashmenu.SplashFont.Menu, n.Dimensions.Width/2, n.Dimensions.Height/2+18, color.White) + + //op = &ebiten.DrawImageOptions{} + //op.ColorScale.Scale(1, 1, 1, n.alpha) + //screen.DrawImage(n.fader, op) + + text.Draw(screen, "Q TO EXIT", splashmenu.SplashFont.Title, n.Dimensions.Width/2-77, n.Dimensions.Height/2, color.RGBA{0xff, 0xff, 0xff, uint8(n.increment / (60 * 2) % 0xff)}) +} + +// register scenevent handler +func (n *Noisy) SetEventHandler(event groovy.SceneEvent, f func()) { + n.events[event] = f +} + +// set the current scene dimensions +func (n *Noisy) SetDimensions(a groovy.Area) { + n.Dimensions = a + n.pixImage = image.NewRGBA(image.Rect(0, 0, a.Width, a.Height)) + n.fader = ebiten.NewImage(a.Width, a.Height) + n.fader.Fill(color.RGBA{0x00, 0x00, 0x00, 0xff}) + + n.pixSubImage = image.NewRGBA(image.Rect(0, 0, n.magnifier.Width, n.magnifier.Height)) + n.subTarget = ebiten.NewImage(n.magnifier.Width, n.magnifier.Height) + n.renderTarget = ebiten.NewImage(n.magnifier.Width, n.magnifier.Height) +} + +func (n *Noisy) UpdateNoise() { + //update cursor position and lock it into the game region bounds + n.sX, n.sY = ebiten.CursorPosition() + n.sX = min(n.sX, n.Dimensions.Width-1) + n.sX = max(n.sX, 0) + n.sY = min(n.sY, n.Dimensions.Height-1) + n.sY = max(n.sY, 0) + + l := n.Dimensions.Width * n.Dimensions.Height + + for i := 0; i < l; i++ { + //x := theRand.next() + //n.pixImage.Pix[4*i] = uint8(x >> 24) + //n.pixImage.Pix[4*i+1] = uint8(x >> 16) + //n.pixImage.Pix[4*i+2] = uint8(x >> 8) + //n.pixImage.Pix[4*i+3] = 0xff + + ballsMode := int(float32(i) * (float32(n.increment) / 60.0)) + + n.pixImage.Pix[4*i] = uint8(n.increment) + n.pixImage.Pix[4*i] = uint8(i % 0xff) + n.pixImage.Pix[4*i+1] = uint8(i >> 16 % 0xff) + n.pixImage.Pix[4*i+2] = uint8(i >> 24 % 0xff) + n.pixImage.Pix[4*i+3] = uint8(ballsMode) % 0xFF + } + + startIdx := n.sX * n.sY * 4 + endIdx := startIdx + n.magnifier.Width*n.magnifier.Height*4 + + if endIdx >= l*4 { + delta := endIdx - l*4 + startIdx = startIdx - delta + endIdx = endIdx - delta + } + + copy(n.pixSubImage.Pix[:], n.pixImage.Pix[startIdx:endIdx]) + + //check for user input to transition scene + if inpututil.IsKeyJustPressed(ebiten.KeyQ) { + if n.events[groovy.COMPLETED] != nil { + n.events[groovy.COMPLETED]() + } + } +} + +//func (r *randy) next() uint32 { +// // math/rand is too slow to keep 60 FPS on web browsers. +// // Use Xorshift instead: http://en.wikipedia.org/wiki/Xorshift +// t := r.x ^ (r.x << 11) +// r.x, r.y, r.z = r.y, r.z, r.w +// r.w = (r.w ^ (r.w >> 19)) ^ (t ^ (t >> 8)) +// return r.w +//} diff --git a/examples/splashmenu/scenes/parallax.go b/examples/splashmenu/scenes/parallax.go new file mode 100644 index 0000000..4c89761 --- /dev/null +++ b/examples/splashmenu/scenes/parallax.go @@ -0,0 +1,254 @@ +package splashmenu + +import ( + "bytes" + "cosmos/diego/groovy" + "image" + "image/color" + "log" + "math" + "math/rand" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" + + _ "embed" +) + +const ( + GALAXY_SCALE_DIST = 50 + DEBRIS_SCALE_DIST = 10 + FLOAT_SCALE = 8 + ASTEROID_COUNT = 14 + MAX_ROTATION_SPEED = 3 + MIN_ROTATION_SPEED = 1 + OFF_SCREEN_SCALE = 2 // number of parallax dimension screens for the logical game display area +) + +var ( + //go:embed assets/galaxy4.jpg + galaxyImg_jpg []byte + + //go:embed assets/debris_00.png + debrisImg_png []byte + + //go:embed assets/roid1.png + roid1Img_png []byte + + //go:embed assets/roid2.png + roid2Img_png []byte + + //go:embed assets/blend.png + blendImg_png []byte + + galaxyBackground *ebiten.Image + debrisImage *ebiten.Image + blendImage *ebiten.Image + blendTmp *ebiten.Image + roids []*ebiten.Image +) + +type Asteroid struct { + cX int + cY int + rotationSpeed float64 +} + +func init() { + // Load up images + img, _, err := image.Decode(bytes.NewReader(galaxyImg_jpg)) + if err != nil { + log.Fatal(err) + } + galaxyBackground = ebiten.NewImageFromImage(img) + + img, _, err = image.Decode(bytes.NewReader(debrisImg_png)) + if err != nil { + log.Fatal(err) + } + debrisImage = ebiten.NewImageFromImage(img) + + //asteroids! + img, _, err = image.Decode(bytes.NewReader(roid1Img_png)) + if err != nil { + log.Fatal(err) + } + roids = append(roids, ebiten.NewImageFromImage(img)) + + img, _, err = image.Decode(bytes.NewReader(roid2Img_png)) + if err != nil { + log.Fatal(err) + } + roids = append(roids, ebiten.NewImageFromImage(img)) + + img, _, err = image.Decode(bytes.NewReader(blendImg_png)) + if err != nil { + log.Fatal(err) + } + blendImage = ebiten.NewImageFromImage(img) + blendTmp = ebiten.NewImageFromImage(blendImage) +} + +type Parallax struct { + events map[groovy.SceneEvent]func() + Dimensions groovy.Area + increment int + asteroids []Asteroid +} + +func NewParallax() Parallax { + return Parallax{ + events: make(map[groovy.SceneEvent]func()), + } +} + +func (p *Parallax) Update() error { + p.increment++ + return nil +} + +func (p *Parallax) Draw(screen *ebiten.Image) { + + p.DrawDynamicBackground(screen) + p.DrawObstacles(screen) + //p.DrawObstaclesWithShamLighting(screen) + + p.RepositionTest(screen) + +} + +func (p *Parallax) SetDimensions(a groovy.Area) { + p.Dimensions = a +} + +func (p *Parallax) SetEventHandler(event groovy.SceneEvent, f func()) { + p.events[event] = f +} + +func (p *Parallax) InitializeParallax() { + + if p.Dimensions.Area() <= 0 { + return + } + + for i := 0; i < ASTEROID_COUNT; i++ { + newX := rand.Intn(p.Dimensions.Width) * OFF_SCREEN_SCALE + newY := rand.Intn(p.Dimensions.Height) * OFF_SCREEN_SCALE + rS := rand.Float64()*MAX_ROTATION_SPEED + MIN_ROTATION_SPEED + p.asteroids = append(p.asteroids, Asteroid{cX: newX, cY: newY, rotationSpeed: rS}) + } + + whiteImage.Fill(color.White) +} + +func (p *Parallax) DrawDynamicBackground(screen *ebiten.Image) { + x, y := ebiten.CursorPosition() + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(-x-p.Dimensions.Width)/GALAXY_SCALE_DIST, float64(-y-p.Dimensions.Height)/GALAXY_SCALE_DIST) + screen.DrawImage(galaxyBackground, op) + + //debris layers + const DEBRIS_QTY = 3 + const DEBRIS_WIDTH = 640 / 3 * 2 + const DEBRIS_HEIGHT = 480 / 3 * 2 + const TOTAL_LAYERS = 2 + for k := 0; k < TOTAL_LAYERS; k++ { + for i := 0; i < DEBRIS_QTY; i++ { + for j := 0; j < DEBRIS_QTY; j++ { + //compute object origins (pre-shifts) + x0 := float64(j*DEBRIS_WIDTH) + float64(p.increment/640) + y0 := float64(i*DEBRIS_HEIGHT) + float64(p.increment/480) + //now compute delta with parallax effect + xd := (x0-float64(x))/DEBRIS_SCALE_DIST*float64(k+1) + x0 - float64(p.increment*k)/FLOAT_SCALE + yd := (y0-float64(y))/DEBRIS_SCALE_DIST*float64(k+1) + y0 - float64(p.increment*k)/FLOAT_SCALE + + op = &ebiten.DrawImageOptions{} + op.GeoM.Translate(xd, yd) + //op.GeoM.Rotate(math.Pi / 180 * float64(p.increment)) + + op.Blend = ebiten.BlendSourceOver + screen.DrawImage(debrisImage, op) + //log.Printf("%d, %d", xoffset, yoffset) + + } + } + } +} + +func (p *Parallax) DrawObstacles(screen *ebiten.Image) { + + x, y := ebiten.CursorPosition() + + for _, a := range p.asteroids { + cx := float64(a.cX) + cy := float64(a.cY) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(-100, -66) + op.GeoM.Rotate(-float64(p.increment) * a.rotationSpeed / (math.Pi * 120)) + op.GeoM.Translate(cx-float64(x), cy-float64(y)) + screen.DrawImage(roids[0], op) + } +} + +func (p *Parallax) DrawObstaclesWithShamLighting(screen *ebiten.Image) { + + x, y := ebiten.CursorPosition() + + for _, a := range p.asteroids { + cx := float64(a.cX) + cy := float64(a.cY) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(-100, -66) + op.GeoM.Rotate(-float64(p.increment) * a.rotationSpeed / (math.Pi * 120)) + op.GeoM.Translate(100, 66) + + //draw roid to temporary location + blendTmp.DrawImage(roids[0], op) + + op = &ebiten.DrawImageOptions{} + op.Blend = ebiten.BlendSourceAtop + blendTmp.DrawImage(blendImage, op) + + op = &ebiten.DrawImageOptions{} + op.GeoM.Translate(cx-float64(x), cy-float64(y)) + screen.DrawImage(blendTmp, op) + + } +} + +func (p *Parallax) RepositionTest(screen *ebiten.Image) { + + var path vector.Path + + //set up origin and boundaries + x0 := float32(p.Dimensions.Width / 2) + y0 := float32(p.Dimensions.Height / 2) + + xd := float32(100.0) + yd := float32(100.0) + + //connect the dots + path.MoveTo(x0-xd, y0-yd) + path.LineTo(x0+xd, y0-yd) + path.LineTo(x0+xd, y0+yd) + path.LineTo(x0-xd, y0+yd) + path.LineTo(x0-xd, y0+yd) + path.Close() + + var vs []ebiten.Vertex + var is []uint16 + + vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = float32(0xff) / 0xFF + vs[i].ColorG = float32(0x00) / 0xFF + vs[i].ColorB = float32(0x00) / 0xFF + vs[i].ColorA = 1 + } + screen.DrawTriangles(vs, is, whiteSubImage, nil) +}