From 8d5144c6e7d939e2e8782a79957e16a65861c02a Mon Sep 17 00:00:00 2001 From: magavel <57044229+magavel@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:59:58 +0900 Subject: [PATCH 1/2] chore: add cursor image --- assets/image/cursor.png | Bin 0 -> 1533 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/image/cursor.png diff --git a/assets/image/cursor.png b/assets/image/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..e9164cb09384cd8389c4677ecd6321e63af2b3c7 GIT binary patch literal 1533 zcmVPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuBXGugsRCod9n~#0sHWbHkCtw71 zf}<1E-5?v(ogi!gHvx14vO%sBm`nhjKxBeu1NZwgPn-!cv9ZM&?(z8{S$h9|Pm=M$ zB^gDQq9`08Qt$;h!`NDygY4+N0k!{vt&k*%u5U{qoq`4MpsRQstUwy03Q1p1Nl>f$ z2&O>&Aa8Wm;1z5QZ|_$Yd&it0+X?vHT3rX((fJD2@9l%a)msqe>qW&DDT@#WI zumm0ik$>$l*VTU~dlGmAN+w87C++0)jIn$J#R%f*WM@p`OcB(990bYfq;aC1dL@?P zKZ78ijyu(iFhZc}sw;3BAxtBe#Lj?nkZ31P8yQ}zM1X}Nndn?rZ%~)I?PNy|+6_;Z zlbTs4hjqp{UdcNWztVL{oEaytX_?*rZFNSq{S-bo=3M0U5j99rZomV{rk5mfw30W( z1J{ewh)$HFyN;f^$owRI2EAsG^kni*2E?so$i3i*Qf0y)P2AxHT*JdrDtf;d z$3Lz8$J%>t{x)(C2a;6914()qywi?2mQ3MXQslzf!11}b(*lSVe8VwtIsy~py0!La zXx2tDtrpkc05r(95n8e;-7@QAj4*4$pQ3zTXt~ll{LJ{P0nX{DF3$ z|Gm|m>p$?ZNBCyO`I`!`v5WLrEyC4#@}UeW!Z18y8_8ZM<4{-9W-Ke_FLRF za4nr_1HCRPZ)VJKH0k24k!>;3IdA%k{x}|ZC+dg*KQ<TM($Ma?Jh4o`w@ zQoAeNYZhSLqH^07x}8*2fSo+BC7jxb^3uY0cwTQ|(pQCTrTS;I~r81|v3s>sCh~D>p+?e2W-xP_E&d8|McVz}dx5`Oo2UWhj8R zBBX;_stC!zGc~T|%{Dv-gs;Nxz5vwp!RorUwtNN%Rk{0P!0LP30qLOt>M$6-COV!t z(z@*nIEIBjeb!)dZk5Szn`V9X@XL*3cA~|Eenq|8T>v*{tM@&R>@| z2C7=PAx`V`hCU95MrXW<>)6d#_>BoZDDNqg)<#e5$ zNucVDM!}yo)(@@Ey%0;pa}Xq_bG7(~3<;V64}#=$(t|?J9r*@|5#)NSUdM$@v>Wui z5RQeC^I!ow7ehY43{?HAKL_KwyqbNI3TD88Aa8WG;Kf>9@819EtJ#at z1bhJwq(EaqWUHUeme{L5Oo5D6?Fo3$RXh%~!_pXRd)onZ$&t8YPOB<}l^xS}pej2q jw5n=f!4^dSBkBJE=C{o#@Kxe`00000NkvXXu0mjfyu`n3 literal 0 HcmV?d00001 From 6fb8437fe0b675fb1da7ea93e940d36d47b14852 Mon Sep 17 00:00:00 2001 From: magavel <57044229+magavel@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:01:17 +0900 Subject: [PATCH 2/2] feat: enhance mouse cursor --- main.go | 21 ++++++++ service/cursor.go | 106 ++++++++++++++++++++++++++++++++++++++ view/components/cursor.go | 72 ++++++++++++++++++++++++++ view/renderer.go | 5 ++ 4 files changed, 204 insertions(+) create mode 100644 service/cursor.go create mode 100644 view/components/cursor.go diff --git a/main.go b/main.go index b125c49..5f2ab28 100644 --- a/main.go +++ b/main.go @@ -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, } } @@ -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) { @@ -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) { @@ -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() diff --git a/service/cursor.go b/service/cursor.go new file mode 100644 index 0000000..b38a53f --- /dev/null +++ b/service/cursor.go @@ -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 +} diff --git a/view/components/cursor.go b/view/components/cursor.go new file mode 100644 index 0000000..a693862 --- /dev/null +++ b/view/components/cursor.go @@ -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) +} diff --git a/view/renderer.go b/view/renderer.go index f5026d2..68bca61 100644 --- a/view/renderer.go +++ b/view/renderer.go @@ -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) +}