451 lines
11 KiB
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
|
|
}
|