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
89 changes: 89 additions & 0 deletions internal/font/fontutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
// 日本語対応フォント
japaneseFontFace *text.GoTextFace
japaneseFontLargeFace *text.GoTextFace
japaneseFontButtonFace *text.GoTextFace // プレイボタン用(32px)
japaneseFontXLargeFace *text.GoTextFace // タイトル用(96px相当)
japaneseFontTitleFace *text.GoTextFace // タイトル用太字(64px相当)
japaneseFontMediumFace *text.GoTextFace // 見出し用(48px相当)
Expand All @@ -40,6 +41,12 @@ func InitFonts() {
Size: 24,
}

// プレイボタン用フォント
japaneseFontButtonFace = &text.GoTextFace{
Source: fontSource,
Size: 32,
}

// 特大サイズのフォント(タイトル用)
japaneseFontXLargeFace = &text.GoTextFace{
Source: fontSource,
Expand Down Expand Up @@ -100,6 +107,8 @@ func DrawJapaneseTextWithSize(screen *ebiten.Image, txt string, x, y float64, fo
font = japaneseFontListFace
case "large":
font = japaneseFontLargeFace
case "button":
font = japaneseFontButtonFace
default:
font = japaneseFontFace
}
Expand Down Expand Up @@ -143,6 +152,8 @@ func MeasureJapaneseTextWithSize(txt string, fontSize string) (width, height flo
font = japaneseFontListFace
case "large":
font = japaneseFontLargeFace
case "button":
font = japaneseFontButtonFace
default:
font = japaneseFontFace
}
Expand All @@ -151,6 +162,36 @@ func MeasureJapaneseTextWithSize(txt string, fontSize string) (width, height flo
return
}

// GetFontMetrics は指定したフォントサイズのメトリクス情報を取得します
func GetFontMetrics(fontSize string) (ascent, descent float64) {
if japaneseFontFace == nil {
InitFonts()
}

var font *text.GoTextFace
switch fontSize {
case "title":
font = japaneseFontTitleFace
case "xlarge":
font = japaneseFontXLargeFace
case "medium":
font = japaneseFontMediumFace
case "list":
font = japaneseFontListFace
case "large":
font = japaneseFontLargeFace
case "button":
font = japaneseFontButtonFace
default:
font = japaneseFontFace
}

metrics := font.Metrics()
ascent = metrics.HAscent
descent = metrics.HDescent
return
}

// DrawCenteredJapaneseText は中央揃えで日本語テキストを描画します
func DrawCenteredJapaneseText(screen *ebiten.Image, txt string, centerX, y float64, large bool, textColor color.Color) {
width, _ := MeasureJapaneseText(txt, large)
Expand All @@ -164,3 +205,51 @@ func DrawCenteredJapaneseTextWithSize(screen *ebiten.Image, txt string, centerX,
x := centerX - width/2
DrawJapaneseTextWithSize(screen, txt, x, y, fontSize, textColor)
}

// DrawCenteredJapaneseTextWithBackground は背景色付きで中央揃えで日本語テキストを描画します
func DrawCenteredJapaneseTextWithBackground(screen *ebiten.Image, txt string, centerX, y float64, large bool, textColor, backgroundColor color.Color) {
width, height := MeasureJapaneseText(txt, large)
x := centerX - width/2

// 背景の描画(パディング付き)
padding := 8.0
backgroundX := x - padding
backgroundY := y - padding
backgroundWidth := width + padding*2
backgroundHeight := height + padding*2

// 背景矩形を描画
backgroundImg := ebiten.NewImage(int(backgroundWidth), int(backgroundHeight))
backgroundImg.Fill(backgroundColor)

backgroundOp := &ebiten.DrawImageOptions{}
backgroundOp.GeoM.Translate(backgroundX, backgroundY)
screen.DrawImage(backgroundImg, backgroundOp)

// テキストを描画
DrawJapaneseText(screen, txt, x, y, large, textColor)
}

// DrawCenteredJapaneseTextWithSizeAndBackground は指定したサイズで背景色付きで中央揃えで日本語テキストを描画します
func DrawCenteredJapaneseTextWithSizeAndBackground(screen *ebiten.Image, txt string, centerX, y float64, fontSize string, textColor, backgroundColor color.Color) {
width, height := MeasureJapaneseTextWithSize(txt, fontSize)
x := centerX - width/2

// 背景の描画(パディング付き)
padding := 8.0
backgroundX := x - padding
backgroundY := y - padding
backgroundWidth := width + padding*2
backgroundHeight := height + padding*2

// 背景矩形を描画
backgroundImg := ebiten.NewImage(int(backgroundWidth), int(backgroundHeight))
backgroundImg.Fill(backgroundColor)

backgroundOp := &ebiten.DrawImageOptions{}
backgroundOp.GeoM.Translate(backgroundX, backgroundY)
screen.DrawImage(backgroundImg, backgroundOp)

// テキストを描画
DrawJapaneseTextWithSize(screen, txt, x, y, fontSize, textColor)
}
31 changes: 30 additions & 1 deletion service/share.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package service
import (
"fmt"
"net/url"
"strconv"
"strings"
)

// GenerateShareURL はX(Twitter)のWeb Intent URLを生成します
Expand All @@ -15,7 +17,7 @@ func GenerateShareURL(score int) string {

// スコアがある場合(ゲームクリア)
if score > 0 {
text := fmt.Sprintf("ゴーファー一意化スコア: %d点!", score)
text := fmt.Sprintf("Go Conference 2025のブースでFind Gopherに挑戦しました!スコア%s点✅", addCommas(score))
params.Add("text", text)
} else {
// スコアがない場合(タイムアウト)
Expand All @@ -30,3 +32,30 @@ func GenerateShareURL(score int) string {

return baseURL + "?" + params.Encode()
}

// addCommas は数値にカンマ区切りを追加します
func addCommas(num int) string {
str := strconv.Itoa(num)
if len(str) <= 3 {
return str
}

var result []string
for i, r := range reverse(str) {
if i > 0 && i%3 == 0 {
result = append(result, ",")
}
result = append(result, string(r))
}

return reverse(strings.Join(result, ""))
}

// reverse は文字列を逆順にします
func reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
59 changes: 53 additions & 6 deletions view/components/button.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ func DrawRoundedButton(screen *ebiten.Image, content string, x, y, w, h int) {
radius = 30 // 最大半径30px
}

// Button body - 黒背景(ホバー時はグレー
buttonColor := color.RGBA{0, 0, 0, 255} //
// Button body - Goブランドカラー背景(ホバー時は少し暗く
buttonColor := color.RGBA{0, 173, 216, 255} // #00ADD8 (Goブランドカラー)
if isHover {
buttonColor = color.RGBA{50, 50, 50, 255} // ダークグレー
buttonColor = color.RGBA{0, 153, 194, 255} // #0099C2 (少し暗め)
}

// 角丸のボタンを描画
Expand All @@ -74,9 +74,56 @@ func DrawRoundedButton(screen *ebiten.Image, content string, x, y, w, h int) {
vector.DrawFilledCircle(screen, float32(x+w)-radius, float32(y+h)-radius, radius, buttonColor, false)

// Button text - 白文字で大きめに
textWidth, textHeight := font.MeasureJapaneseTextWithSize(content, "large")
textWidth, _ := font.MeasureJapaneseTextWithSize(content, "large")
ascent, _ := font.GetFontMetrics("large")

contentX := float64(x) + (float64(w)-textWidth)/2
// ボタンの垂直中央に配置
contentY := float64(y) + float64(h)/2 - textHeight/3
// ボタンの垂直中央に配置(上側に微調整)
textCenterY := float64(y) + float64(h)/2
contentY := textCenterY - ascent*0.6
font.DrawJapaneseTextWithSize(screen, content, contentX, contentY, "large", color.White)
}

// DrawRoundedButtonWithFontSize は指定したフォントサイズで丸みを帯びたボタンを描画します
// x, y: ボタンの左上座標
// w, h: ボタンの幅と高さ
// fontSize: フォントサイズタイプ
func DrawRoundedButtonWithFontSize(screen *ebiten.Image, content string, x, y, w, h int, fontSize string) {
// Mouse hover detection
mx, my := ebiten.CursorPosition()
isHover := mx >= x && mx <= x+w && my >= y && my <= y+h

// 角丸の半径
radius := float32(h / 2) // ボタンの高さの半分を角丸の半径に
if radius > 30 {
radius = 30 // 最大半径30px
}

// Button body - Goブランドカラー背景(ホバー時は少し暗く)
buttonColor := color.RGBA{0, 173, 216, 255} // #00ADD8 (Goブランドカラー)
if isHover {
buttonColor = color.RGBA{0, 153, 194, 255} // #0099C2 (少し暗め)
}

// 角丸のボタンを描画
// 中央の長方形
vector.DrawFilledRect(screen, float32(x)+radius, float32(y), float32(w)-2*radius, float32(h), buttonColor, false)
// 左右の長方形
vector.DrawFilledRect(screen, float32(x), float32(y)+radius, radius, float32(h)-2*radius, buttonColor, false)
vector.DrawFilledRect(screen, float32(x+w)-radius, float32(y)+radius, radius, float32(h)-2*radius, buttonColor, false)
// 角の円(簡易的に円で代替)
vector.DrawFilledCircle(screen, float32(x)+radius, float32(y)+radius, radius, buttonColor, false)
vector.DrawFilledCircle(screen, float32(x+w)-radius, float32(y)+radius, radius, buttonColor, false)
vector.DrawFilledCircle(screen, float32(x)+radius, float32(y+h)-radius, radius, buttonColor, false)
vector.DrawFilledCircle(screen, float32(x+w)-radius, float32(y+h)-radius, radius, buttonColor, false)

// Button text - 白文字で指定のフォントサイズ
textWidth, _ := font.MeasureJapaneseTextWithSize(content, fontSize)
ascent, _ := font.GetFontMetrics(fontSize)

contentX := float64(x) + (float64(w)-textWidth)/2
// ボタンの垂直中央に配置(上側に微調整)
textCenterY := float64(y) + float64(h)/2
contentY := textCenterY - ascent*0.6
font.DrawJapaneseTextWithSize(screen, content, contentX, contentY, fontSize, color.White)
}
8 changes: 3 additions & 5 deletions view/renderer.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package view

import (
"image/color"

"github.com/hajimehoshi/ebiten/v2"
"github.com/snkrdunk/find-gopher/internal/font"
"github.com/snkrdunk/find-gopher/service"
Expand All @@ -29,11 +27,11 @@ func (r *Renderer) Draw(screen *ebiten.Image) {
screens.DrawTitleScreen(screen, r.game)

case service.StateGame:
// Game background color (white)
screen.Fill(color.RGBA{255, 255, 255, 255})
// 先に背景とUIを描画
screens.DrawGameScreen(screen, r.game)
// その後にGopherとエフェクトを描画
DrawGophers(screen, r.game.Gophers)
components.DrawEffects(screen, r.game.EffectManager.GetAll())
screens.DrawGameScreen(screen, r.game)

case service.StateResult:
screens.DrawResultScreen(screen, r.game)
Expand Down
7 changes: 5 additions & 2 deletions view/screens/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import (

// DrawGameScreen はゲーム画面を描画します
func DrawGameScreen(screen *ebiten.Image, game *service.Game) {
// ダークグレー背景
screen.Fill(color.RGBA{50, 50, 50, 255})

// ゲームコンテナ(枠)を描画
drawGameContainer(screen, game)

// タイマー表示を中央上部に配置(960x540画面に適合)
timerText := fmt.Sprintf("残り時間%.1f秒", game.TimeLeft)
timerColor := color.RGBA{0, 0, 0, 255} // デフォルトは黒
timerColor := color.RGBA{255, 255, 255, 255} // デフォルトは白
if game.EffectManager.HasTimerRedEffect() {
timerColor = color.RGBA{255, 0, 0, 255} // 赤文字エフェクト中は赤
}
Expand All @@ -43,7 +46,7 @@ func drawGameContainer(screen *ebiten.Image, game *service.Game) {
)

// エフェクト状態に応じて枠色を決定
borderColor := color.RGBA{0, 0, 0, 255} // デフォルトは黒
borderColor := color.RGBA{255, 255, 255, 255} // デフォルトは白
if game.EffectManager.HasSuccessEffect() {
borderColor = color.RGBA{0, 255, 0, 255} // 成功時は緑
} else if game.EffectManager.HasPenaltyEffect() {
Expand Down
30 changes: 17 additions & 13 deletions view/screens/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ func DrawResultScreen(screen *ebiten.Image, game *service.Game) {

// drawSuccessScreen は成功時の画面を描画します
func drawSuccessScreen(screen *ebiten.Image, game *service.Game) {
// 白背景
screen.Fill(color.White)
// ダークグレー背景
screen.Fill(color.RGBA{50, 50, 50, 255})

// 背景Gopherの初期化と更新
if !backgroundInitialized {
Expand All @@ -84,14 +84,15 @@ func drawSuccessScreen(screen *ebiten.Image, game *service.Game) {
// 背景Gopherを描画
drawBackgroundGophers(screen)

// タイトル「ゲームクリア!」
font.DrawCenteredJapaneseText(screen, "ゲームクリア!", service.ScreenWidth/2, successTitleY, true, color.Black)
// タイトル「ゲームクリア!」(背景色付き)
backgroundColor := color.RGBA{0, 0, 0, 180}
font.DrawCenteredJapaneseTextWithBackground(screen, "ゲームクリア!", service.ScreenWidth/2, successTitleY, true, color.White, backgroundColor)

// スコア表示(2行)
// スコア表示(2行)(背景色付き)
scoreText1 := fmt.Sprintf("スコア:%d", game.Score)
scoreText2 := fmt.Sprintf("(残り時間%.1f秒)", float64(game.Score)/1000.0)
font.DrawCenteredJapaneseText(screen, scoreText1, service.ScreenWidth/2, successScoreY, true, color.Black)
font.DrawCenteredJapaneseText(screen, scoreText2, service.ScreenWidth/2, successScoreY+35, false, color.Black)
font.DrawCenteredJapaneseTextWithBackground(screen, scoreText1, service.ScreenWidth/2, successScoreY, true, color.White, backgroundColor)
font.DrawCenteredJapaneseText(screen, scoreText2, service.ScreenWidth/2, successScoreY+35, false, color.White)

// QRコードの生成と表示
if qrCodeImage == nil || cachedScore != game.Score {
Expand All @@ -109,8 +110,10 @@ func drawSuccessScreen(screen *ebiten.Image, game *service.Game) {
op.GeoM.Translate(float64(qrX), float64(successQRCodeY))
screen.DrawImage(qrCodeImage, op)

// QRコード説明テキスト
font.DrawCenteredJapaneseText(screen, "QRコードをスキャンしてXにシェア", service.ScreenWidth/2, successQRDescY, false, color.Black)
// QRコード説明テキスト(点滅・背景色付き)
if int(time.Now().UnixMilli()/500)%2 == 0 {
font.DrawCenteredJapaneseTextWithBackground(screen, "QRコードをスキャンしてXにシェア", service.ScreenWidth/2, successQRDescY, false, color.White, backgroundColor)
}
}

// ボタン描画
Expand All @@ -119,11 +122,12 @@ func drawSuccessScreen(screen *ebiten.Image, game *service.Game) {

// drawFailureScreen は失敗時の画面を描画します
func drawFailureScreen(screen *ebiten.Image, game *service.Game) {
// 白背景
screen.Fill(color.White)
// ダークグレー背景
screen.Fill(color.RGBA{50, 50, 50, 255})

// タイトル「ざんねん!タイムアップ!」
font.DrawCenteredJapaneseText(screen, "ざんねん!タイムアップ!", service.ScreenWidth/2, failTitleY, true, color.Black)
// タイトル「ざんねん!タイムアップ!」(背景色付き)
backgroundColor := color.RGBA{0, 0, 0, 180}
font.DrawCenteredJapaneseTextWithBackground(screen, "ざんねん!タイムアップ!", service.ScreenWidth/2, failTitleY, true, color.White, backgroundColor)

// 中央に大きなWANTED Gopher画像
if service.WantedGopherImage != nil && service.WantedGopherImage.Image != nil {
Expand Down
Loading
Loading