maptile/util/sprite/sprite.go

451 lines
11 KiB
Go

package sprite
import (
"bytes"
"encoding/json"
"fmt"
"image"
"image/png"
"math"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/srwiley/oksvg"
"github.com/srwiley/rasterx"
"golang.org/x/image/draw"
)
type Sprite struct {
name string
infos []*ImageInfo
Root *SpriteNode
}
type ImageInfo struct {
Name string
Path, Type string
Width, Height float64
// image image.Image
Fit *SpriteNode
}
type SpriteBuilder struct {
outPath string
sprites []Sprite
}
type SpriteNode struct {
X float64
Y float64
Width float64
Height float64
Used bool
Down *SpriteNode
Right *SpriteNode
}
func NewSpriteBuilder(iconPath string, outPath string) (*SpriteBuilder, error) {
basePath, err := os.Executable()
if err != nil {
return nil, errors.Errorf("could not get execute path: %v", err)
}
defaultOutPath := "style/sprite"
if outPath == "" {
outPath = defaultOutPath
}
if err := os.MkdirAll(filepath.Join(filepath.Dir(basePath), outPath), 0644); err != nil {
return nil, err
}
files, err := os.ReadDir(iconPath)
if err != nil {
panic(err)
}
sb := &SpriteBuilder{
outPath: filepath.Join(filepath.Dir(basePath), outPath),
sprites: []Sprite{},
}
for _, file := range files {
if !file.IsDir() {
continue
}
subFiles, err := os.ReadDir(filepath.Join(iconPath, file.Name()))
if err != nil {
continue
}
imageInfos := []*ImageInfo{}
for _, subFile := range subFiles {
if subFile.IsDir() {
continue
}
filePath := filepath.Join(iconPath, file.Name(), subFile.Name())
fileName := filepath.Base(filePath)
fileType := filepath.Ext(filePath)
if fileType != ".svg" && fileType != ".png" {
continue
}
imageInfo := ImageInfo{
Name: strings.TrimSuffix(fileName, fileType),
Type: fileType,
Path: filePath,
}
imageInfos = append(imageInfos, &imageInfo)
}
sb.sprites = append(sb.sprites, Sprite{
name: file.Name(),
infos: imageInfos,
Root: nil,
})
}
return sb, nil
}
func (sb *SpriteBuilder) Build() error {
for _, sprite := range sb.sprites {
if _, err := os.Stat(filepath.Join(sb.outPath, sprite.name)); err != nil {
err = os.MkdirAll(filepath.Join(sb.outPath, sprite.name), 0644)
if err != nil {
return err
}
}
sprite.Fit()
if err := sprite.Save(sb.outPath, 1); err != nil {
return err
}
if err := sprite.Save(sb.outPath, 2); err != nil {
return err
}
}
return nil
}
func (s *Sprite) Fit() {
if len(s.infos) == 0 {
return
}
// 初始化根节点,使用第一个块的尺寸
firstBlock := s.infos[0]
firstBlock.ReadInfo()
s.Root = &SpriteNode{
X: 0,
Y: 0,
Width: firstBlock.Width,
Height: firstBlock.Height,
}
for _, info := range s.infos {
info.ReadInfo()
node := s.findNode(s.Root, info.Width, info.Height)
if node != nil {
// 找到合适的位置,分割节点
fit := s.splitNode(node, info.Width, info.Height)
info.Fit = fit
} else {
// 没有找到合适位置,扩展容器
fit := s.growNode(info.Width, info.Height)
info.Fit = fit
}
}
}
// findNode 查找可以容纳指定尺寸的节点
func (s *Sprite) findNode(root *SpriteNode, width, height float64) *SpriteNode {
if root.Used {
// 如果当前节点已使用,递归查找子节点
if right := s.findNode(root.Right, width, height); right != nil {
return right
}
if down := s.findNode(root.Down, width, height); down != nil {
return down
}
return nil
} else if width <= root.Width && height <= root.Height {
// 当前节点可以容纳该块
return root
}
return nil
}
// splitNode 分割节点,创建新的子节点
func (s *Sprite) splitNode(node *SpriteNode, width, height float64) *SpriteNode {
node.Used = true
node.Down = &SpriteNode{
X: node.X,
Y: node.Y + height,
Width: node.Width,
Height: node.Height - height,
}
node.Right = &SpriteNode{
X: node.X + width,
Y: node.Y,
Width: node.Width - width,
Height: height,
}
return node
}
// growNode 扩展容器
func (s *Sprite) growNode(width, height float64) *SpriteNode {
canGrowDown := width <= s.Root.Width
canGrowRight := height <= s.Root.Height
shouldGrowRight := canGrowRight && (s.Root.Height >= (s.Root.Width + width)) // 保持方形,优先向右扩展
shouldGrowDown := canGrowDown && (s.Root.Width >= (s.Root.Height + height)) // 保持方形,优先向下扩展
if shouldGrowRight {
return s.growRight(width, height)
} else if shouldGrowDown {
return s.growDown(width, height)
} else if canGrowRight {
return s.growRight(width, height)
} else if canGrowDown {
return s.growDown(width, height)
}
return nil // 应该确保初始根节点尺寸合理以避免这种情况
}
// growRight 向右扩展容器
func (s *Sprite) growRight(width, height float64) *SpriteNode {
newRoot := &SpriteNode{
Used: true,
X: 0,
Y: 0,
Width: s.Root.Width + width,
Height: s.Root.Height,
Down: s.Root,
Right: &SpriteNode{
X: s.Root.Width,
Y: 0,
Width: width,
Height: s.Root.Height,
},
}
s.Root = newRoot
if node := s.findNode(s.Root, width, height); node != nil {
return s.splitNode(node, width, height)
}
return nil
}
// growDown 向下扩展容器
func (s *Sprite) growDown(width, height float64) *SpriteNode {
newRoot := &SpriteNode{
Used: true,
X: 0,
Y: 0,
Width: s.Root.Width,
Height: s.Root.Height + height,
Down: &SpriteNode{
X: 0,
Y: s.Root.Height,
Width: s.Root.Width,
Height: height,
},
Right: s.Root,
}
s.Root = newRoot
if node := s.findNode(s.Root, width, height); node != nil {
return s.splitNode(node, width, height)
}
return nil
}
func (s *Sprite) Save(outPath string, pixelRatio int) error {
img := image.NewRGBA(image.Rect(0, 0, int(s.Root.Width)*pixelRatio, int(s.Root.Height)*pixelRatio))
for _, info := range s.infos {
// fmt.Println(info.Name, info.Fit.X, info.Fit.Y, info.Fit.Width, info.Fit.Height)
// draw.Draw(img, info.image.Bounds().Add(image.Pt(int(info.Fit.X), int(info.Fit.Y))), info.image, image.Point{}, draw.Over)
infoImg := info.ReadImg(pixelRatio)
if infoImg == nil {
continue
}
draw.Draw(img, infoImg.Bounds().Add(image.Pt(int(info.Fit.X)*pixelRatio, int(info.Fit.Y)*pixelRatio)), infoImg, image.Point{}, draw.Over)
// if pixelRatio != 1 {
// minX := info.image.Bounds().Min.X
// minY := info.image.Bounds().Min.Y
// maxX := info.image.Bounds().Max.X
// maxY := info.image.Bounds().Max.Y
// imgXRect := image.Rect(minX, minY, minX+(maxX-minX)*pixelRatio, minY+(maxY-minY)*pixelRatio)
// draw.ApproxBiLinear.Scale(img, imgXRect.Add(image.Pt(int(info.Fit.X)*pixelRatio, int(info.Fit.Y)*pixelRatio)), info.image, info.image.Bounds(), draw.Src, nil)
// } else {
// draw.Draw(img, info.image.Bounds().Add(image.Pt(int(info.Fit.X), int(info.Fit.Y))), info.image, image.Point{}, draw.Over)
// }
}
fileName := s.name
if pixelRatio != 1 {
fileName = fmt.Sprintf("%s@%dx", s.name, pixelRatio)
}
pngFilePath := filepath.Join(outPath, s.name, fileName+".png")
var pngBuffer bytes.Buffer
err := png.Encode(&pngBuffer, img)
if err != nil {
return err
}
err = os.WriteFile(pngFilePath, pngBuffer.Bytes(), 0644)
if err != nil {
return err
}
jsonFilePath := filepath.Join(outPath, s.name, fileName+".json")
jsonMap := make(map[string]map[string]interface{})
for _, img := range s.infos {
jsonMap[img.Name] = map[string]interface{}{
"width": img.Width * float64(pixelRatio),
"height": img.Height * float64(pixelRatio),
"pixelRatio": pixelRatio,
"x": img.Fit.X * float64(pixelRatio),
"y": img.Fit.Y * float64(pixelRatio),
}
}
jsonBuffer, err := json.MarshalIndent(jsonMap, "", " ")
if err != nil {
return err
}
err = os.WriteFile(jsonFilePath, jsonBuffer, 0644)
if err != nil {
return err
}
return nil
}
func (i *ImageInfo) ReadInfo() {
file, err := os.Open(i.Path)
if err != nil {
return
}
defer file.Close()
if i.Type == ".png" {
pngImg, err := png.Decode(file)
if err != nil {
return
}
width := pngImg.Bounds().Dx()
height := pngImg.Bounds().Dy()
i.Width = float64(width)
i.Height = float64(height)
// pngImgx := image.NewRGBA(image.Rect(0, 0, width, height))
// draw.ApproxBiLinear.Scale(pngImgx, pngImgx.Bounds(), pngImg, pngImg.Bounds(), draw.Src, nil)
// i.image = pngImgx
return
}
if i.Type == ".svg" {
svgInfo, err := oksvg.ReadIconStream(file, oksvg.WarnErrorMode)
if err != nil {
return
}
// baseScale := 8.0
baseWidth := math.Ceil(svgInfo.ViewBox.W)
baseHeight := math.Ceil(svgInfo.ViewBox.H)
// svgImg := image.NewRGBA(image.Rect(0, 0, int(baseWidth*baseScale), int(baseHeight*baseScale)))
// draw.Draw(svgImg, svgImg.Bounds(), image.Transparent, image.Point{}, draw.Src)
// svgInfo.SetTarget(-svgInfo.ViewBox.X*baseScale, -svgInfo.ViewBox.Y*baseScale, svgInfo.ViewBox.W*baseScale, svgInfo.ViewBox.H*baseScale)
// scaner := rasterx.NewScannerGV(int(baseWidth*baseScale), int(baseHeight*baseScale), svgImg, svgImg.Bounds())
// raster := rasterx.NewDasher(int(baseWidth*baseScale), int(baseHeight*baseScale), scaner)
// svgInfo.Draw(raster, 1.0)
i.Width = baseWidth
i.Height = baseHeight
// svgImgx := image.NewRGBA(image.Rect(0, 0, int(i.Width), int(i.Height)))
// draw.ApproxBiLinear.Scale(svgImgx, svgImgx.Bounds(), svgImg, svgImg.Bounds(), draw.Src, nil)
// i.image = svgImgx
return
}
}
func (i *ImageInfo) ReadImg(pixelRatio int) image.Image {
file, err := os.Open(i.Path)
if err != nil {
return nil
}
defer file.Close()
if i.Type == ".png" {
pngImg, err := png.Decode(file)
if err != nil {
return nil
}
width := pngImg.Bounds().Dx() * pixelRatio
height := pngImg.Bounds().Dy() * pixelRatio
pngImgx := image.NewRGBA(image.Rect(0, 0, width, height))
draw.ApproxBiLinear.Scale(pngImgx, pngImgx.Bounds(), pngImg, pngImg.Bounds(), draw.Src, nil)
return pngImgx
}
if i.Type == ".svg" {
svgInfo, err := oksvg.ReadIconStream(file, oksvg.WarnErrorMode)
if err != nil {
return nil
}
baseScale := 8.0
baseWidth := math.Ceil(svgInfo.ViewBox.W)
baseHeight := math.Ceil(svgInfo.ViewBox.H)
svgImg := image.NewRGBA(image.Rect(0, 0, int(baseWidth*baseScale), int(baseHeight*baseScale)))
draw.Draw(svgImg, svgImg.Bounds(), image.Transparent, image.Point{}, draw.Src)
svgInfo.SetTarget(-svgInfo.ViewBox.X*baseScale, -svgInfo.ViewBox.Y*baseScale, svgInfo.ViewBox.W*baseScale, svgInfo.ViewBox.H*baseScale)
scaner := rasterx.NewScannerGV(int(baseWidth*baseScale), int(baseHeight*baseScale), svgImg, svgImg.Bounds())
raster := rasterx.NewDasher(int(baseWidth*baseScale), int(baseHeight*baseScale), scaner)
svgInfo.Draw(raster, 1.0)
svgImgx := image.NewRGBA(image.Rect(0, 0, int(baseWidth)*pixelRatio, int(baseHeight)*pixelRatio))
draw.ApproxBiLinear.Scale(svgImgx, svgImgx.Bounds(), svgImg, svgImg.Bounds(), draw.Src, nil)
return svgImgx
}
return nil
}