|
|
package font
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"fmt"
|
|
|
"image/color"
|
|
|
"io/fs"
|
|
|
"log"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
|
|
"golang.org/x/image/font/gofont/goregular"
|
|
|
)
|
|
|
|
|
|
// FontRenderer handles text rendering with assets/fonts directory support
|
|
|
type FontRenderer struct {
|
|
|
face *text.GoTextFace
|
|
|
}
|
|
|
|
|
|
// NewFontRenderer creates a new font renderer using only assets/fonts directory
|
|
|
func NewFontRenderer() *FontRenderer {
|
|
|
var fontData []byte
|
|
|
var fontSource string
|
|
|
|
|
|
// 优先加载 assets/fonts 目录中的字体
|
|
|
if data, source := loadFromAssetsDirectory(); data != nil {
|
|
|
fontData = data
|
|
|
fontSource = source
|
|
|
log.Printf("✅ 使用项目字体: %s", source)
|
|
|
} else {
|
|
|
// 如果没有找到项目字体,使用 Go 默认字体
|
|
|
fontData = goregular.TTF
|
|
|
fontSource = "Go默认字体 (建议添加中文字体到 assets/fonts 目录)"
|
|
|
log.Printf("⚠️ 未找到项目字体,使用默认字体。建议将中文字体放入 assets/fonts/ 目录")
|
|
|
}
|
|
|
|
|
|
// 创建字体源
|
|
|
source, err := createFontSource(fontData, fontSource)
|
|
|
if err != nil {
|
|
|
// 最终回退 - 使用 Go 默认字体
|
|
|
log.Printf("❌ 字体加载失败,回退到默认字体: %v", err)
|
|
|
source, err = text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF))
|
|
|
if err != nil {
|
|
|
panic(fmt.Sprintf("无法加载默认字体: %v", err))
|
|
|
}
|
|
|
fontSource = "Go默认字体 (回退)"
|
|
|
}
|
|
|
|
|
|
face := &text.GoTextFace{
|
|
|
Source: source,
|
|
|
Size: 16,
|
|
|
}
|
|
|
|
|
|
log.Printf("🎨 字体系统初始化完成,使用: %s", fontSource)
|
|
|
return &FontRenderer{
|
|
|
face: face,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// loadFromAssetsDirectory 专门扫描 assets/fonts 目录并加载字体文件
|
|
|
func loadFromAssetsDirectory() ([]byte, string) {
|
|
|
// 尝试多个可能的路径
|
|
|
possiblePaths := []string{
|
|
|
"assets/fonts", // 开发环境
|
|
|
"Contents/Resources/assets/fonts", // macOS 应用包内部
|
|
|
"../Resources/assets/fonts", // macOS 应用包相对路径
|
|
|
filepath.Join(getExecutableDir(), "assets/fonts"), // 可执行文件同目录
|
|
|
}
|
|
|
|
|
|
var assetsPath string
|
|
|
var found bool
|
|
|
|
|
|
// 查找存在的资源路径
|
|
|
for _, path := range possiblePaths {
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
|
assetsPath = path
|
|
|
found = true
|
|
|
log.Printf("📁 找到字体目录: %s", path)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if !found {
|
|
|
log.Printf("📁 未找到 assets/fonts 目录,已尝试路径: %v", possiblePaths)
|
|
|
return nil, ""
|
|
|
}
|
|
|
|
|
|
// 扫描目录下的所有字体文件
|
|
|
var fontFiles []string
|
|
|
err := filepath.WalkDir(assetsPath, func(path string, d fs.DirEntry, err error) error {
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
if !d.IsDir() {
|
|
|
ext := strings.ToLower(filepath.Ext(path))
|
|
|
if ext == ".ttf" || ext == ".ttc" || ext == ".otf" {
|
|
|
fontFiles = append(fontFiles, path)
|
|
|
}
|
|
|
}
|
|
|
return nil
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
log.Printf("❌ 扫描字体目录失败: %v", err)
|
|
|
return nil, ""
|
|
|
}
|
|
|
|
|
|
if len(fontFiles) == 0 {
|
|
|
log.Printf("📂 字体目录为空,请添加字体文件 (.ttf, .ttc, .otf)")
|
|
|
return nil, ""
|
|
|
}
|
|
|
|
|
|
log.Printf("🔍 在 %s 找到 %d 个字体文件", assetsPath, len(fontFiles))
|
|
|
|
|
|
// 字体优先级:微软雅黑 > 其他中文字体 > 任意字体
|
|
|
fontPriorities := [][]string{
|
|
|
{"msyh", "yahei", "microsoft"}, // 微软雅黑系列
|
|
|
{"noto", "source", "思源", "源"}, // 思源/Noto系列
|
|
|
{"pingfang", "hiragino", "苹方"}, // 苹果字体系列
|
|
|
{"simhei", "simkai", "simsun", "宋", "黑"}, // 系统中文字体
|
|
|
}
|
|
|
|
|
|
// 按优先级查找字体
|
|
|
for _, priorityGroup := range fontPriorities {
|
|
|
for _, keyword := range priorityGroup {
|
|
|
for _, fontFile := range fontFiles {
|
|
|
fileName := strings.ToLower(filepath.Base(fontFile))
|
|
|
if strings.Contains(fileName, keyword) {
|
|
|
if data, err := tryLoadFont(fontFile); err == nil {
|
|
|
log.Printf("🎯 找到优先字体 (%s): %s", keyword, fontFile)
|
|
|
return data, fontFile
|
|
|
} else {
|
|
|
log.Printf("⚠️ 字体文件损坏或不支持: %s, 错误: %v", fontFile, err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果没有找到优先字体,使用第一个可用的字体
|
|
|
for _, fontFile := range fontFiles {
|
|
|
if data, err := tryLoadFont(fontFile); err == nil {
|
|
|
log.Printf("📝 使用可用字体: %s", fontFile)
|
|
|
return data, fontFile
|
|
|
} else {
|
|
|
log.Printf("⚠️ 跳过不可用字体: %s, 错误: %v", fontFile, err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
log.Printf("❌ 字体目录中没有可用的字体文件")
|
|
|
return nil, ""
|
|
|
}
|
|
|
|
|
|
// getExecutableDir 获取可执行文件所在目录
|
|
|
func getExecutableDir() string {
|
|
|
execPath, err := os.Executable()
|
|
|
if err != nil {
|
|
|
log.Printf("⚠️ 获取可执行文件路径失败: %v", err)
|
|
|
return "."
|
|
|
}
|
|
|
return filepath.Dir(execPath)
|
|
|
}
|
|
|
|
|
|
// tryLoadFont 尝试加载字体文件并验证格式
|
|
|
func tryLoadFont(fontPath string) ([]byte, error) {
|
|
|
data, err := os.ReadFile(fontPath)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("读取字体文件失败: %w", err)
|
|
|
}
|
|
|
|
|
|
// 验证字体数据是否可以被解析
|
|
|
_, err = createFontSource(data, fontPath)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("字体格式验证失败: %w", err)
|
|
|
}
|
|
|
|
|
|
return data, nil
|
|
|
}
|
|
|
|
|
|
// createFontSource 创建字体源,处理可能的collection错误
|
|
|
func createFontSource(fontData []byte, fontSource string) (*text.GoTextFaceSource, error) {
|
|
|
// 尝试直接加载
|
|
|
source, err := text.NewGoTextFaceSource(bytes.NewReader(fontData))
|
|
|
if err != nil {
|
|
|
// 如果是 collection 错误,提供更详细的信息
|
|
|
errStr := err.Error()
|
|
|
if strings.Contains(errStr, "collection") {
|
|
|
return nil, fmt.Errorf("TTC集合字体文件不支持,请使用TTF格式: %s", filepath.Base(fontSource))
|
|
|
}
|
|
|
return nil, fmt.Errorf("字体文件格式错误: %w", err)
|
|
|
}
|
|
|
|
|
|
return source, nil
|
|
|
}
|
|
|
|
|
|
// DrawText draws text at the specified position with proper Chinese support
|
|
|
func (fr *FontRenderer) DrawText(screen *ebiten.Image, str string, x, y float64, clr color.Color) {
|
|
|
op := &text.DrawOptions{}
|
|
|
op.GeoM.Translate(x, y)
|
|
|
op.ColorScale.ScaleWithColor(clr)
|
|
|
text.Draw(screen, str, fr.face, op)
|
|
|
}
|
|
|
|
|
|
// SetFontSize sets the font size
|
|
|
func (fr *FontRenderer) SetFontSize(size float64) {
|
|
|
fr.face.Size = size
|
|
|
}
|
|
|
|
|
|
// MeasureText returns text dimensions with better accuracy
|
|
|
func (fr *FontRenderer) MeasureText(str string) (float64, float64) {
|
|
|
width, _ := text.Measure(str, fr.face, 0)
|
|
|
return width, fr.face.Size
|
|
|
}
|
|
|
|
|
|
// GetFontInfo returns information about the currently loaded font
|
|
|
func (fr *FontRenderer) GetFontInfo() string {
|
|
|
return fmt.Sprintf("字体大小: %.1f", fr.face.Size)
|
|
|
}
|
|
|
|