Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/image/cursor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,23 @@ var wantedGopherData []byte
//go:embed assets/audio/bgm.wav
var bgmData []byte

//go:embed assets/image/cursor.png
var cursorData []byte

type Game struct {
gameService *service.Game
renderer *view.Renderer
cursor *service.Cursor
}

func NewGame() *Game {
gameService := service.NewGame()
renderer := view.NewRenderer(gameService)
cursor := service.NewCursor()
return &Game{
gameService: gameService,
renderer: renderer,
cursor: cursor,
}
}

Expand All @@ -55,6 +61,13 @@ func (g *Game) Update() error {
dt := now.Sub(g.gameService.LastUpdate).Seconds()
g.gameService.LastUpdate = now

// カーソル位置を取得
mx, my := ebiten.CursorPosition()
isClicking := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)

// カーソルを更新
g.cursor.Update(mx, my, isClicking)

switch g.gameService.State {
case service.StateTitle:
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
Expand Down Expand Up @@ -92,6 +105,7 @@ func (g *Game) Update() error {

func (g *Game) Draw(screen *ebiten.Image) {
g.renderer.Draw(screen)
g.renderer.DrawCursor(screen, g.cursor)
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
Expand All @@ -111,9 +125,16 @@ func main() {
bgmData,
)

// Load cursor image
if err := service.LoadCursorImage(cursorData); err != nil {
log.Printf("Failed to load cursor image: %v", err)
}

// Set window properties
ebiten.SetWindowSize(service.ScreenWidth, service.ScreenHeight)
ebiten.SetWindowTitle("Find Gopher")
// デフォルトカーソルを非表示
ebiten.SetCursorMode(ebiten.CursorModeHidden)

// Create and run game
game := NewGame()
Expand Down
106 changes: 106 additions & 0 deletions service/cursor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package service

import (
"bytes"
"image"
"time"

"github.com/hajimehoshi/ebiten/v2"
)

// Trail はカーソルのトレイル効果の一つの要素を表します
type Trail struct {
X float64
Y float64
Alpha float64
Scale float64
Created time.Time
}

// Cursor はカスタムカーソルを管理する構造体です
type Cursor struct {
Image *ebiten.Image
X float64
Y float64
LastX float64
LastY float64
Trails []Trail
TrailMaxAge time.Duration
IsClicking bool
ClickScale float64
BaseScale float64
CurrentScale float64
}

var cursorImage *ebiten.Image

// LoadCursorImage はカーソル画像を読み込みます
func LoadCursorImage(data []byte) error {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return err
}
cursorImage = ebiten.NewImageFromImage(img)
return nil
}

// NewCursor は新しいカーソルインスタンスを作成します
func NewCursor() *Cursor {
return &Cursor{
Image: cursorImage,
Trails: make([]Trail, 0),
TrailMaxAge: 500 * time.Millisecond,
BaseScale: 1.0,
ClickScale: 0.8,
CurrentScale: 1.0,
}
}

// Update はカーソルの状態を更新します
func (c *Cursor) Update(x, y int, isClicking bool) {
c.X = float64(x)
c.Y = float64(y)
c.IsClicking = isClicking

// カーソルが移動した距離を計算
dx := c.X - c.LastX
dy := c.Y - c.LastY
distance := dx*dx + dy*dy

now := time.Now()

// 移動距離が閾値(2ピクセル)以上の場合のみトレイルを追加
if distance >= 4.0 { // 2*2 = 4
c.Trails = append(c.Trails, Trail{
X: c.X,
Y: c.Y,
Alpha: 1.0,
Scale: 1.0,
Created: now,
})
// 前回の位置を更新
c.LastX = c.X
c.LastY = c.Y
}

// 古いトレイルを削除
newTrails := make([]Trail, 0)
for i := range c.Trails {
age := now.Sub(c.Trails[i].Created)
if age < c.TrailMaxAge {
// アルファ値とスケールを時間経過で減少
progress := age.Seconds() / c.TrailMaxAge.Seconds()
c.Trails[i].Alpha = 1.0 - progress
c.Trails[i].Scale = 1.0 - progress*0.5
newTrails = append(newTrails, c.Trails[i])
}
}
c.Trails = newTrails

// スケールをアニメーション(拡大効果を削除)
targetScale := c.BaseScale
if isClicking {
targetScale = c.ClickScale
}
c.CurrentScale += (targetScale - c.CurrentScale) * 0.2
}
72 changes: 72 additions & 0 deletions view/components/cursor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package components

import (
"image/color"

"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
"github.com/snkrdunk/find-gopher/service"
)

// DrawCursor はカスタムカーソルとその効果を描画します
func DrawCursor(screen *ebiten.Image, cursor *service.Cursor) {
if cursor == nil || cursor.Image == nil {
return
}

// トレイル効果を描画
drawCursorTrails(screen, cursor)

// カーソル本体を描画
drawCursorImage(screen, cursor)
}

// drawCursorTrails はカーソルのトレイル効果を描画します
func drawCursorTrails(screen *ebiten.Image, cursor *service.Cursor) {
for _, trail := range cursor.Trails {
if trail.Alpha <= 0 {
continue
}

// トレイルの円を描画
radius := float32(6 * trail.Scale)
alpha := uint8(trail.Alpha * 100)
trailColor := color.RGBA{150, 200, 250, alpha}

vector.DrawFilledCircle(
screen,
float32(trail.X),
float32(trail.Y),
radius,
trailColor,
true,
)
}
}

// drawCursorImage はカーソル画像を描画します
func drawCursorImage(screen *ebiten.Image, cursor *service.Cursor) {
if cursor.Image == nil {
return
}

opts := &ebiten.DrawImageOptions{}

// カーソル画像のサイズを取得
bounds := cursor.Image.Bounds()
w := float64(bounds.Dx())
h := float64(bounds.Dy())

// 中心を基準に変換
opts.GeoM.Translate(-w/2, -h/2)

// 64x64の画像を32x32に縮小(0.5倍)
baseScale := 0.5
// スケール(baseScale * CurrentScale)
opts.GeoM.Scale(baseScale*cursor.CurrentScale, baseScale*cursor.CurrentScale)

// カーソル位置に移動
opts.GeoM.Translate(cursor.X, cursor.Y)

screen.DrawImage(cursor.Image, opts)
}
5 changes: 5 additions & 0 deletions view/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ func (r *Renderer) Draw(screen *ebiten.Image) {
func (r *Renderer) Layout(outsideWidth, outsideHeight int) (int, int) {
return service.ScreenWidth, service.ScreenHeight
}

// DrawCursor はカスタムカーソルを描画します
func (r *Renderer) DrawCursor(screen *ebiten.Image, cursor *service.Cursor) {
components.DrawCursor(screen, cursor)
}
Loading