feat: 添加图像处理功能,包含精灵管理、文本渲染和SVG字体支持,更新README文档以描述新功能,添加.gitignore文件以排除不必要的文件。
This commit is contained in:
parent
1346911f86
commit
2c2f855198
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
test.png
|
||||
face.png
|
||||
.vscode/launch.json
|
188
README.md
188
README.md
@ -1,3 +1,189 @@
|
||||
# imagedd
|
||||
|
||||
golang绘图库
|
||||
golang绘图库
|
||||
|
||||
## 功能概述
|
||||
|
||||
imagedd是一个Go语言绘图库,提供了精灵图(Sprite)管理、2D图形绘制、图像渲染等功能。
|
||||
|
||||
## 主要组件
|
||||
|
||||
### 精灵图 (Sprite)
|
||||
|
||||
精灵图是基本的渲染单元,包含图像数据和位置信息。
|
||||
|
||||
```go
|
||||
type Sprite struct {
|
||||
Name string // 精灵名称
|
||||
Position image.Point // 精灵位置
|
||||
Image image.Image // 精灵图像
|
||||
Index int // 用于排序的索引
|
||||
}
|
||||
```
|
||||
|
||||
#### 精灵图操作
|
||||
|
||||
```go
|
||||
// 移动精灵
|
||||
sprite.Move(x, y int)
|
||||
|
||||
// 旋转精灵
|
||||
sprite.Rotate(angle float64)
|
||||
|
||||
// 投影变换
|
||||
sprite.Project(projectMatrix *ProjectMatrix)
|
||||
|
||||
// 在精灵上绘制线条
|
||||
sprite.DrawLine(line *Line)
|
||||
```
|
||||
|
||||
### 精灵板 (NamedSpriteBoard)
|
||||
|
||||
精灵板用于管理多个精灵,提供高效的添加、查找、排序功能。
|
||||
|
||||
```go
|
||||
// 创建新的精灵板
|
||||
board := sprite.NewNamedSpriteBoard()
|
||||
|
||||
// 添加精灵
|
||||
board.AddSprite(sprite)
|
||||
|
||||
// 按名称查找精灵
|
||||
foundSprite := board.GetSpriteByName("精灵名称")
|
||||
|
||||
// 获取特定索引的所有精灵
|
||||
sprites := board.GetSpritesByIndex(1)
|
||||
|
||||
// 按名称删除精灵
|
||||
board.RemoveSpriteByName("精灵名称")
|
||||
|
||||
// 更新精灵的索引
|
||||
board.UpdateSpriteIndex("精灵名称", 2)
|
||||
|
||||
// 更新精灵的名称
|
||||
board.UpdateSpriteName("旧名称", "新名称")
|
||||
|
||||
// 获取所有精灵(按Index主排序,Name次排序)
|
||||
allSprites := board.GetAllSprites()
|
||||
|
||||
// 渲染精灵板为图像
|
||||
img := board.RenderToImage()
|
||||
|
||||
// 保存为PNG文件
|
||||
board.SaveToPng("output.png")
|
||||
```
|
||||
|
||||
### 2D图形组件
|
||||
|
||||
#### 线条 (Line)
|
||||
|
||||
```go
|
||||
// 创建线条
|
||||
line := &sprite.Line{
|
||||
Start: image.Point{X: 10, Y: 10},
|
||||
End: image.Point{X: 100, Y: 100},
|
||||
Width: 2,
|
||||
Color: color.RGBA{255, 0, 0, 255}, // 红色
|
||||
}
|
||||
|
||||
// 添加到精灵
|
||||
line.AddToSprite(sprite)
|
||||
```
|
||||
|
||||
#### 圆形 (Circle)
|
||||
|
||||
```go
|
||||
// 创建圆形
|
||||
circle := &sprite.Circle{
|
||||
Center: image.Point{X: 50, Y: 50},
|
||||
Radius: 30,
|
||||
Color: color.RGBA{0, 0, 255, 255}, // 蓝色
|
||||
}
|
||||
|
||||
// 添加到精灵
|
||||
circle.AddToSprite(sprite)
|
||||
```
|
||||
|
||||
#### 投影矩阵 (ProjectMatrix)
|
||||
|
||||
```go
|
||||
// 创建投影矩阵
|
||||
matrix := &sprite.ProjectMatrix{
|
||||
Matrix: [2][2]float64{
|
||||
{1.0, 0.5},
|
||||
{0.0, 1.0},
|
||||
},
|
||||
}
|
||||
|
||||
// 投影点
|
||||
newPoint := matrix.ProjectPoint(image.Point{X: 10, Y: 20})
|
||||
|
||||
// 投影线条
|
||||
newLine := matrix.ProjectLine(line)
|
||||
```
|
||||
|
||||
## 渲染特性
|
||||
|
||||
- 支持精灵的层级渲染(通过Index控制)
|
||||
- 支持精灵的名称索引(O(1)时间复杂度查找)
|
||||
- 支持负坐标位置的精灵
|
||||
- 自动计算画布大小以适应所有精灵
|
||||
- 处理超出边界的精灵(裁剪)
|
||||
|
||||
## 使用示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"git.lxtend.com/lixiangwuxian/imagedd/sprite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建精灵板
|
||||
board := sprite.NewNamedSpriteBoard()
|
||||
|
||||
// 创建精灵
|
||||
sprite1 := &sprite.Sprite{
|
||||
Name: "背景",
|
||||
Position: image.Point{0, 0},
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
// 创建圆形并添加到精灵
|
||||
circle := &sprite.Circle{
|
||||
Center: image.Point{50, 50},
|
||||
Radius: 30,
|
||||
Color: color.RGBA{0, 0, 255, 255},
|
||||
}
|
||||
circle.AddToSprite(sprite1)
|
||||
|
||||
// 添加到精灵板
|
||||
board.AddSprite(sprite1)
|
||||
|
||||
// 创建第二个精灵
|
||||
sprite2 := &sprite.Sprite{
|
||||
Name: "线条",
|
||||
Position: image.Point{20, 20},
|
||||
Index: 1,
|
||||
}
|
||||
|
||||
// 创建线条并添加到精灵
|
||||
line := &sprite.Line{
|
||||
Start: image.Point{0, 0},
|
||||
End: image.Point{100, 100},
|
||||
Width: 2,
|
||||
Color: color.RGBA{255, 0, 0, 255},
|
||||
}
|
||||
line.AddToSprite(sprite2)
|
||||
|
||||
// 添加到精灵板
|
||||
board.AddSprite(sprite2)
|
||||
|
||||
// 渲染并保存
|
||||
board.SaveToPng("output.png")
|
||||
}
|
||||
```
|
13046
font2img/NotoEmoji-VariableFont_wght.svg
Normal file
13046
font2img/NotoEmoji-VariableFont_wght.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 2.6 MiB |
BIN
font2img/fusion-pixel-12px-monospaced.ttf
Normal file
BIN
font2img/fusion-pixel-12px-monospaced.ttf
Normal file
Binary file not shown.
886
font2img/parse_font.go
Normal file
886
font2img/parse_font.go
Normal file
@ -0,0 +1,886 @@
|
||||
package font2img
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/srwiley/oksvg"
|
||||
"github.com/srwiley/rasterx"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed NotoEmoji-VariableFont_wght.svg
|
||||
var svgEmojiBytes []byte
|
||||
|
||||
//go:embed fusion-pixel-12px-monospaced.ttf
|
||||
var ttfBytes []byte
|
||||
|
||||
// 全局emoji字体和相关变量
|
||||
var (
|
||||
svgEmojiPath string
|
||||
svgEmojiLock sync.RWMutex
|
||||
|
||||
// 添加SVG缓存,避免重复解析相同的SVG文件
|
||||
svgCache map[rune][]byte
|
||||
svgCacheLock sync.RWMutex
|
||||
|
||||
// 保存已加载的SVG字体文件
|
||||
svgFontData []byte
|
||||
svgFontLock sync.RWMutex
|
||||
svgFontCache *SVG
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 初始化SVG缓存
|
||||
svgCache = make(map[rune][]byte)
|
||||
|
||||
// 初始化嵌入的SVG字体
|
||||
var svg SVG
|
||||
if err := xml.Unmarshal(svgEmojiBytes, &svg); err == nil {
|
||||
svgFontLock.Lock()
|
||||
svgFontData = svgEmojiBytes
|
||||
svgFontCache = &svg
|
||||
svgFontLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SVG表示一个简单的SVG解析结构
|
||||
type SVG struct {
|
||||
Width int `xml:"width,attr"`
|
||||
Height int `xml:"height,attr"`
|
||||
ViewBox string `xml:"viewBox,attr"`
|
||||
Defs struct {
|
||||
Font struct {
|
||||
Glyphs []struct {
|
||||
GlyphName string `xml:"glyph-name,attr"`
|
||||
Unicode string `xml:"unicode,attr"`
|
||||
VertAdvY string `xml:"vert-adv-y,attr"`
|
||||
Path string `xml:"d,attr"`
|
||||
} `xml:"glyph"`
|
||||
} `xml:"font"`
|
||||
} `xml:"defs"`
|
||||
}
|
||||
|
||||
// SetSVGEmojiPath 设置SVG格式emoji的字体文件路径
|
||||
func SetSVGEmojiPath(path string) error {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return errors.New("SVG emoji字体文件不存在")
|
||||
}
|
||||
|
||||
svgEmojiLock.Lock()
|
||||
defer svgEmojiLock.Unlock()
|
||||
|
||||
// 清除之前的缓存
|
||||
svgFontLock.Lock()
|
||||
svgFontData = nil
|
||||
svgFontCache = nil
|
||||
svgFontLock.Unlock()
|
||||
|
||||
svgCacheLock.Lock()
|
||||
svgCache = make(map[rune][]byte)
|
||||
svgCacheLock.Unlock()
|
||||
|
||||
svgEmojiPath = path
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSVGEmoji 加载特定码点的SVG Emoji
|
||||
func loadSVGEmoji(codePoint rune) ([]byte, error) {
|
||||
// 先从缓存中查找
|
||||
svgCacheLock.RLock()
|
||||
if data, ok := svgCache[codePoint]; ok {
|
||||
svgCacheLock.RUnlock()
|
||||
return data, nil
|
||||
}
|
||||
svgCacheLock.RUnlock()
|
||||
|
||||
// 缓存中没有,从SVG字体文件加载
|
||||
svgFontLock.RLock()
|
||||
fontData := svgFontData
|
||||
fontCache := svgFontCache
|
||||
svgFontLock.RUnlock()
|
||||
|
||||
// 如果没有字体数据,且有指定路径,则从文件加载
|
||||
if len(fontData) == 0 {
|
||||
svgEmojiLock.RLock()
|
||||
path := svgEmojiPath
|
||||
svgEmojiLock.RUnlock()
|
||||
|
||||
if path != "" {
|
||||
// 读取SVG字体文件
|
||||
var err error
|
||||
fontData, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析SVG字体
|
||||
var svg SVG
|
||||
if err := xml.Unmarshal(fontData, &svg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存到缓存
|
||||
svgFontLock.Lock()
|
||||
svgFontData = fontData
|
||||
svgFontCache = &svg
|
||||
fontCache = &svg
|
||||
svgFontLock.Unlock()
|
||||
} else {
|
||||
// 没有外部路径,直接使用嵌入的字体数据
|
||||
fontData = svgEmojiBytes
|
||||
|
||||
// 如果未初始化,则解析嵌入的字体
|
||||
if fontCache == nil {
|
||||
var svg SVG
|
||||
if err := xml.Unmarshal(fontData, &svg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svgFontLock.Lock()
|
||||
svgFontCache = &svg
|
||||
fontCache = &svg
|
||||
svgFontLock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在字体中查找匹配的字形
|
||||
targetChar := string(codePoint)
|
||||
|
||||
if fontCache != nil {
|
||||
for _, glyph := range fontCache.Defs.Font.Glyphs {
|
||||
if glyph.Unicode == targetChar {
|
||||
// 创建包含该字形的SVG数据
|
||||
glyphSVG := createGlyphSVG(glyph, fontCache)
|
||||
|
||||
// 添加到缓存
|
||||
svgCacheLock.Lock()
|
||||
svgCache[codePoint] = []byte(glyphSVG)
|
||||
svgCacheLock.Unlock()
|
||||
|
||||
return []byte(glyphSVG), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("未找到匹配的SVG字形")
|
||||
}
|
||||
|
||||
// createGlyphSVG 从字形数据创建独立的SVG
|
||||
func createGlyphSVG(glyph struct {
|
||||
GlyphName string `xml:"glyph-name,attr"`
|
||||
Unicode string `xml:"unicode,attr"`
|
||||
VertAdvY string `xml:"vert-adv-y,attr"`
|
||||
Path string `xml:"d,attr"`
|
||||
}, fontInfo *SVG) string {
|
||||
// 获取视口尺寸
|
||||
viewBox := fontInfo.ViewBox
|
||||
if viewBox == "" {
|
||||
viewBox = "0 0 1000 1000" // 默认视口
|
||||
}
|
||||
|
||||
// 创建独立的SVG标记
|
||||
svgTemplate := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="%s">
|
||||
<path d="%s" fill="currentColor"/>
|
||||
</svg>`
|
||||
|
||||
return fmt.Sprintf(svgTemplate, viewBox, glyph.Path)
|
||||
}
|
||||
|
||||
// renderSVGEmojiToImage 将SVG转换为图像
|
||||
func renderSVGEmojiToImage(svgData []byte, size float64, col color.Color) (*image.RGBA, error) {
|
||||
// 解析SVG以获取尺寸信息
|
||||
var svg SVG
|
||||
if err := xml.Unmarshal(svgData, &svg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 准备输入数据
|
||||
r, g, b, _ := col.RGBA()
|
||||
hexColor := rgbaToHex(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||
|
||||
// 替换currentColor为指定的颜色
|
||||
svgStr := string(svgData)
|
||||
svgStr = strings.ReplaceAll(svgStr, "currentColor", hexColor)
|
||||
|
||||
// 创建输出图像
|
||||
imgSize := int(size)
|
||||
if imgSize < 10 {
|
||||
imgSize = 10
|
||||
}
|
||||
|
||||
// 创建一个透明背景的RGBA图像
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, imgSize, imgSize))
|
||||
draw.Draw(rgba, rgba.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||||
|
||||
// 直接渲染SVG数据到图像
|
||||
err := renderSVGDataToImage([]byte(svgStr), rgba)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rgba, nil
|
||||
}
|
||||
|
||||
// renderSVGDataToImage 使用oksvg库渲染SVG数据到图像
|
||||
func renderSVGDataToImage(svgData []byte, dst *image.RGBA) error {
|
||||
// 从字节数据创建SVG图标
|
||||
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData), oksvg.StrictErrorMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置图标大小适应目标图像
|
||||
bounds := dst.Bounds()
|
||||
width, height := float64(bounds.Dx()), float64(bounds.Dy())
|
||||
|
||||
// 创建转换后的图像(先渲染到这里,然后再处理翻转)
|
||||
tempImg := image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy()))
|
||||
|
||||
// 适应目标大小
|
||||
icon.SetTarget(0, width/8, width/2.7, height/2.7)
|
||||
|
||||
// 创建光栅化器
|
||||
scanner := rasterx.NewScannerGV(bounds.Dx(), bounds.Dy(), tempImg, tempImg.Bounds())
|
||||
raster := rasterx.NewDasher(bounds.Dx(), bounds.Dy(), scanner)
|
||||
|
||||
// 渲染图标
|
||||
icon.Draw(raster, 1.0)
|
||||
|
||||
// 只垂直翻转图像(处理上下颠倒问题)
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
for x := 0; x < bounds.Dx(); x++ {
|
||||
// 计算垂直翻转后的坐标
|
||||
flippedY := bounds.Dy() - y - 1
|
||||
|
||||
// 将翻转后的像素复制到目标图像
|
||||
dst.Set(x, y, tempImg.At(x, flippedY))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rgbaToHex 将RGB颜色分量转换为十六进制字符串
|
||||
func rgbaToHex(r, g, b uint8) string {
|
||||
return "#" + colorToHex(r) + colorToHex(g) + colorToHex(b)
|
||||
}
|
||||
|
||||
// colorToHex 将单个颜色分量转换为十六进制
|
||||
func colorToHex(c uint8) string {
|
||||
hex := "0123456789ABCDEF"
|
||||
return string(hex[c>>4]) + string(hex[c&0x0F])
|
||||
}
|
||||
|
||||
// IsEmoji 检测一个字符是否是emoji
|
||||
func IsEmoji(r rune) bool {
|
||||
// 简单检测一些常见的emoji范围
|
||||
return unicode.Is(unicode.So, r) || // Symbol, Other
|
||||
// 表情符号范围
|
||||
(r >= 0x1F600 && r <= 0x1F64F) || // Emoticons
|
||||
(r >= 0x1F300 && r <= 0x1F5FF) || // Misc Symbols and Pictographs
|
||||
(r >= 0x1F680 && r <= 0x1F6FF) || // Transport and Map
|
||||
(r >= 0x1F700 && r <= 0x1F77F) || // Alchemical Symbols
|
||||
(r >= 0x1F780 && r <= 0x1F7FF) || // Geometric Shapes
|
||||
(r >= 0x1F800 && r <= 0x1F8FF) || // Supplemental Arrows-C
|
||||
(r >= 0x1F900 && r <= 0x1F9FF) || // Supplemental Symbols and Pictographs
|
||||
(r >= 0x1FA00 && r <= 0x1FA6F) || // Chess Symbols
|
||||
(r >= 0x1FA70 && r <= 0x1FAFF) || // Symbols and Pictographs Extended-A
|
||||
// 国旗符号
|
||||
(r >= 0x1F1E6 && r <= 0x1F1FF)
|
||||
}
|
||||
|
||||
// HasGlyph 检查字体是否包含该字符的字形
|
||||
func HasGlyph(fontContent *truetype.Font, char rune) bool {
|
||||
if fontContent == nil {
|
||||
return false
|
||||
}
|
||||
// 尝试获取字符索引
|
||||
index := fontContent.Index(char)
|
||||
return index != 0 // 索引0通常是notdef字形
|
||||
}
|
||||
|
||||
// SafeGlyphAdvance 安全获取字符宽度,避免崩溃
|
||||
func SafeGlyphAdvance(face font.Face, char rune) (fixed.Int26_6, bool) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 发生panic,忽略
|
||||
}
|
||||
}()
|
||||
|
||||
return face.GlyphAdvance(char)
|
||||
}
|
||||
|
||||
// SafeGlyphBounds 安全获取字符边界,避免崩溃
|
||||
func SafeGlyphBounds(face font.Face, char rune) (fixed.Rectangle26_6, fixed.Int26_6, bool) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 发生panic,忽略
|
||||
}
|
||||
}()
|
||||
|
||||
return face.GlyphBounds(char)
|
||||
}
|
||||
|
||||
// SafeDrawString 安全绘制字符串,避免崩溃
|
||||
func SafeDrawString(ctx *freetype.Context, text string, pt fixed.Point26_6) (fixed.Point26_6, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 发生panic,忽略
|
||||
}
|
||||
}()
|
||||
|
||||
return ctx.DrawString(text, pt)
|
||||
}
|
||||
|
||||
// ParseFont 解析字体文件或使用内嵌字体
|
||||
func ParseFont(fontPath string) (*truetype.Font, error) {
|
||||
// 如果提供了路径,尝试从文件加载
|
||||
if fontPath != "" {
|
||||
fontBytes, err := os.ReadFile(fontPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fontCache, err := truetype.Parse(fontBytes)
|
||||
return fontCache, err
|
||||
}
|
||||
|
||||
// 没有提供路径,使用嵌入的TTF字体
|
||||
fontCache, err := truetype.Parse(ttfBytes)
|
||||
return fontCache, err
|
||||
}
|
||||
|
||||
// RenderCharToImage 将单个字符渲染为图像
|
||||
// fontSize: 字体大小(点数)
|
||||
// textColor: 字体颜色
|
||||
// 返回字符的图像
|
||||
func RenderCharToImage(fontContent *truetype.Font, char rune, fontSize float64, textColor color.Color) (*image.RGBA, error) {
|
||||
// 确保字体已加载
|
||||
if fontContent == nil {
|
||||
// 尝试使用嵌入的字体
|
||||
var err error
|
||||
fontContent, err = truetype.Parse(ttfBytes)
|
||||
if err != nil {
|
||||
return nil, ErrFontNotLoaded
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是emoji,且应该使用SVG格式
|
||||
if IsEmoji(char) {
|
||||
// 尝试加载SVG emoji
|
||||
svgData, err := loadSVGEmoji(char)
|
||||
if err == nil {
|
||||
// 成功加载SVG,渲染为图像
|
||||
return renderSVGEmojiToImage(svgData, fontSize, textColor)
|
||||
}
|
||||
// 如果加载失败,回退到使用普通emoji字体
|
||||
return createPlaceholderImage(fontSize, textColor), nil
|
||||
}
|
||||
|
||||
// 创建字体上下文
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72) // 标准DPI
|
||||
c.SetFont(fontContent)
|
||||
c.SetFontSize(fontSize)
|
||||
c.SetHinting(font.HintingFull)
|
||||
|
||||
// 计算字符尺寸
|
||||
opts := truetype.Options{
|
||||
Size: fontSize,
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
}
|
||||
face := truetype.NewFace(fontContent, &opts)
|
||||
|
||||
// 测量单个字符宽度
|
||||
charWidth, ok := SafeGlyphAdvance(face, char)
|
||||
if !ok {
|
||||
// 无法获取字符宽度,使用替代图像
|
||||
return createPlaceholderImage(fontSize, textColor), nil
|
||||
}
|
||||
widthInPixels := charWidth.Ceil()
|
||||
if widthInPixels <= 0 {
|
||||
widthInPixels = int(fontSize / 2)
|
||||
}
|
||||
|
||||
// 计算字体高度和基线位置
|
||||
bounds, _, ok := SafeGlyphBounds(face, char)
|
||||
if !ok {
|
||||
// 无法获取字符边界,使用替代图像
|
||||
return createPlaceholderImage(fontSize, textColor), nil
|
||||
}
|
||||
height := (bounds.Max.Y - bounds.Min.Y).Ceil()
|
||||
if height <= 0 {
|
||||
height = int(fontSize)
|
||||
}
|
||||
|
||||
// 添加少量额外空间确保字符完全显示
|
||||
heightInPixels := height + int(fontSize/8)
|
||||
|
||||
// 创建图像,确保有足够空间
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, widthInPixels, heightInPixels))
|
||||
draw.Draw(rgba, rgba.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||||
|
||||
// 设置绘图属性
|
||||
c.SetDst(rgba)
|
||||
c.SetClip(rgba.Bounds())
|
||||
c.SetSrc(image.NewUniform(textColor))
|
||||
|
||||
// 计算正确的基线位置
|
||||
// 基线应该在字符下降部分的上方
|
||||
baseline := (-bounds.Min.Y).Ceil()
|
||||
|
||||
// 绘制字符,Y坐标需要考虑一个偏移以确保完整显示
|
||||
pt := fixed.Point26_6{
|
||||
X: fixed.Int26_6(0 << 6),
|
||||
Y: fixed.Int26_6(baseline << 6),
|
||||
}
|
||||
_, err := SafeDrawString(c, string(char), pt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 裁剪图像,去除多余的空白
|
||||
trimmedRGBA := trimImage(rgba)
|
||||
return trimmedRGBA, nil
|
||||
}
|
||||
|
||||
// trimImage 裁剪图像,去除底部、顶部、左侧和右侧的空白
|
||||
func trimImage(img *image.RGBA) *image.RGBA {
|
||||
bounds := img.Bounds()
|
||||
min, max := bounds.Min, bounds.Max
|
||||
|
||||
// 找到字符的实际底部(最后一个非透明像素的行)
|
||||
bottom := min.Y
|
||||
for y := max.Y - 1; y >= min.Y; y-- {
|
||||
isEmpty := true
|
||||
for x := min.X; x < max.X; x++ {
|
||||
_, _, _, a := img.At(x, y).RGBA()
|
||||
if a > 0 {
|
||||
isEmpty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isEmpty {
|
||||
bottom = y + 1 // 找到最后一个非空行
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 找到字符的实际顶部(第一个非透明像素的行)
|
||||
top := max.Y - 1
|
||||
for y := min.Y; y < max.Y; y++ {
|
||||
isEmpty := true
|
||||
for x := min.X; x < max.X; x++ {
|
||||
_, _, _, a := img.At(x, y).RGBA()
|
||||
if a > 0 {
|
||||
isEmpty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isEmpty {
|
||||
top = y // 找到第一个非空行
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 找到字符的实际左侧边缘(第一个非透明像素的列)
|
||||
left := max.X - 1
|
||||
for x := min.X; x < max.X; x++ {
|
||||
isEmpty := true
|
||||
for y := min.Y; y < max.Y; y++ {
|
||||
_, _, _, a := img.At(x, y).RGBA()
|
||||
if a > 0 {
|
||||
isEmpty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isEmpty {
|
||||
left = x // 找到第一个非空列
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 找到字符的实际右侧边缘(最后一个非透明像素的列)
|
||||
right := min.X
|
||||
for x := max.X - 1; x >= min.X; x-- {
|
||||
isEmpty := true
|
||||
for y := min.Y; y < max.Y; y++ {
|
||||
_, _, _, a := img.At(x, y).RGBA()
|
||||
if a > 0 {
|
||||
isEmpty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isEmpty {
|
||||
right = x + 1 // 找到最后一个非空列
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 防止空图像情况
|
||||
if top >= bottom || left >= right {
|
||||
return img // 返回原图
|
||||
}
|
||||
|
||||
// 创建新图像并复制内容
|
||||
newWidth := right - left
|
||||
newHeight := bottom - top
|
||||
newImg := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
||||
for y := 0; y < newHeight; y++ {
|
||||
for x := 0; x < newWidth; x++ {
|
||||
newImg.Set(x, y, img.At(x+left, y+top))
|
||||
}
|
||||
}
|
||||
|
||||
return newImg
|
||||
}
|
||||
|
||||
// RenderTextToImage 将一段文本渲染为图像
|
||||
// text: 要渲染的文本,支持换行符
|
||||
// fontSize: 字体大小(点数)
|
||||
// textColor: 字体颜色
|
||||
// charSpacing: 字符间距(像素)
|
||||
// lineSpacing: 行间距(像素)
|
||||
// 返回渲染后的图像
|
||||
func RenderTextToImage(fontContent *truetype.Font, text string, fontSize float64, textColor color.Color, charSpacing, lineSpacing int) (*image.RGBA, error) {
|
||||
// 确保字体已加载
|
||||
if fontContent == nil {
|
||||
// 尝试使用嵌入的字体
|
||||
var err error
|
||||
fontContent, err = truetype.Parse(ttfBytes)
|
||||
if err != nil {
|
||||
return nil, ErrFontNotLoaded
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有SVG emoji数据
|
||||
hasSVGData := false
|
||||
svgFontLock.RLock()
|
||||
hasSVGData = svgFontCache != nil
|
||||
svgFontLock.RUnlock()
|
||||
|
||||
// 首先需要计算整个文本的尺寸
|
||||
lines := splitTextIntoLines(text)
|
||||
if len(lines) == 0 {
|
||||
return image.NewRGBA(image.Rect(0, 0, 1, 1)), nil // 返回1x1空图像
|
||||
}
|
||||
|
||||
// 创建字体配置
|
||||
opts := truetype.Options{
|
||||
Size: fontSize,
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
}
|
||||
face := truetype.NewFace(fontContent, &opts)
|
||||
|
||||
// 计算每行的宽度和整体高度
|
||||
var maxWidth int
|
||||
lineHeights := make([]int, len(lines))
|
||||
lineBounds := make([]fixed.Rectangle26_6, len(lines))
|
||||
|
||||
for i, line := range lines {
|
||||
var lineWidth int
|
||||
// 计算一行的宽度
|
||||
for _, char := range line {
|
||||
// 决定使用哪个字体
|
||||
currentFace := face
|
||||
|
||||
// 处理SVG emoji
|
||||
if hasSVGData && IsEmoji(char) {
|
||||
// 对于SVG emoji,使用固定宽度
|
||||
lineWidth += int(fontSize) + charSpacing
|
||||
continue
|
||||
}
|
||||
|
||||
advance, ok := SafeGlyphAdvance(currentFace, char)
|
||||
if !ok {
|
||||
// 如果无法获取宽度,使用默认宽度
|
||||
lineWidth += int(fontSize/2) + charSpacing
|
||||
continue
|
||||
}
|
||||
lineWidth += advance.Ceil() + charSpacing
|
||||
}
|
||||
|
||||
// 如果一行有字符,减去最后一个字符后多加的间距
|
||||
if len(line) > 0 {
|
||||
lineWidth -= charSpacing
|
||||
}
|
||||
|
||||
// 计算行的边界,用于高度计算
|
||||
lineBound := fixed.Rectangle26_6{
|
||||
Min: fixed.Point26_6{X: 0, Y: fixed.Int26_6(-int(fontSize) << 6)},
|
||||
Max: fixed.Point26_6{X: 0, Y: fixed.Int26_6(int(fontSize) << 6)},
|
||||
}
|
||||
|
||||
for _, char := range line {
|
||||
// 如果是SVG emoji,跳过边界计算
|
||||
if hasSVGData && IsEmoji(char) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 决定使用哪个字体
|
||||
currentFace := face
|
||||
|
||||
bound, _, ok := SafeGlyphBounds(currentFace, char)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if bound.Min.Y < lineBound.Min.Y {
|
||||
lineBound.Min.Y = bound.Min.Y
|
||||
}
|
||||
if bound.Max.Y > lineBound.Max.Y {
|
||||
lineBound.Max.Y = bound.Max.Y
|
||||
}
|
||||
}
|
||||
|
||||
lineBounds[i] = lineBound
|
||||
lineHeight := int(fontSize)
|
||||
lineHeights[i] = lineHeight
|
||||
|
||||
if lineWidth > maxWidth {
|
||||
maxWidth = lineWidth
|
||||
}
|
||||
}
|
||||
|
||||
// 防止创建太小的图像
|
||||
if maxWidth <= 0 {
|
||||
maxWidth = int(fontSize)
|
||||
}
|
||||
|
||||
// 计算总高度(每行高度+行间距)
|
||||
totalHeight := 0
|
||||
for _, height := range lineHeights {
|
||||
totalHeight += height + lineSpacing + 1
|
||||
}
|
||||
// 减去最后一行多加的行间距
|
||||
if len(lineHeights) > 0 {
|
||||
totalHeight -= lineSpacing
|
||||
}
|
||||
|
||||
// 防止创建太小的图像
|
||||
if totalHeight <= 0 {
|
||||
totalHeight = int(fontSize)
|
||||
}
|
||||
|
||||
// 创建图像
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, maxWidth, totalHeight))
|
||||
draw.Draw(rgba, rgba.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||||
|
||||
// 设置字体上下文
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72)
|
||||
c.SetFontSize(fontSize)
|
||||
c.SetHinting(font.HintingFull)
|
||||
c.SetDst(rgba)
|
||||
c.SetClip(rgba.Bounds())
|
||||
c.SetSrc(image.NewUniform(textColor))
|
||||
|
||||
// 渲染每一行文本
|
||||
y := 0
|
||||
for i, line := range lines {
|
||||
if len(line) == 0 {
|
||||
y += lineHeights[i] + lineSpacing
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算基线位置
|
||||
baseline := y + (-lineBounds[i].Min.Y).Ceil()
|
||||
|
||||
// 绘制每个字符
|
||||
x := 0
|
||||
for _, char := range line {
|
||||
// 如果是SVG格式的emoji,单独处理
|
||||
if hasSVGData && IsEmoji(char) {
|
||||
svgData, err := loadSVGEmoji(char)
|
||||
if err == nil {
|
||||
// 渲染SVG emoji
|
||||
emojiImg, err := renderSVGEmojiToImage(svgData, fontSize, textColor)
|
||||
if err == nil {
|
||||
emojiBounds := emojiImg.Bounds()
|
||||
draw.Draw(rgba,
|
||||
image.Rect(x, baseline-int(fontSize), x+emojiBounds.Dx(), baseline),
|
||||
emojiImg,
|
||||
image.Point{},
|
||||
draw.Over)
|
||||
x += emojiBounds.Dx() + charSpacing
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 如果加载或渲染SVG失败,回退到使用普通方式
|
||||
}
|
||||
|
||||
// 决定使用哪个字体
|
||||
currentFont := fontContent
|
||||
currentFace := face
|
||||
useCurrentFont := true
|
||||
|
||||
if useCurrentFont {
|
||||
// 设置当前字符的字体
|
||||
c.SetFont(currentFont)
|
||||
|
||||
// 绘制字符
|
||||
pt := fixed.Point26_6{
|
||||
X: fixed.Int26_6(x << 6),
|
||||
Y: fixed.Int26_6(baseline << 6),
|
||||
}
|
||||
|
||||
width, ok := SafeGlyphAdvance(currentFace, char)
|
||||
if !ok {
|
||||
// 无法获取宽度,使用默认宽度
|
||||
x += int(fontSize/2) + charSpacing
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := SafeDrawString(c, string(char), pt)
|
||||
if err != nil {
|
||||
// 绘制失败,使用替代方法
|
||||
drawPlaceholder(rgba, x, baseline, int(fontSize), textColor)
|
||||
x += int(fontSize/2) + charSpacing
|
||||
continue
|
||||
}
|
||||
|
||||
// 移动到下一个字符位置
|
||||
x += width.Ceil() + charSpacing
|
||||
} else {
|
||||
// 不能使用当前字体,绘制占位符
|
||||
drawPlaceholder(rgba, x, baseline, int(fontSize/2), textColor)
|
||||
x += int(fontSize/2) + charSpacing
|
||||
}
|
||||
}
|
||||
|
||||
// 移动到下一行
|
||||
y += lineHeights[i] + lineSpacing
|
||||
}
|
||||
|
||||
return rgba, nil
|
||||
}
|
||||
|
||||
// RenderTextToTrimmedImage 将一段文本渲染为图像并裁剪多余空白
|
||||
// 与RenderTextToImage功能相同,但会自动裁剪图像边缘的空白
|
||||
func RenderTextToTrimmedImage(fontContent *truetype.Font, text string, fontSize float64, textColor color.Color, charSpacing, lineSpacing int) (*image.RGBA, error) {
|
||||
// 如果字体为nil,尝试使用嵌入的字体
|
||||
if fontContent == nil {
|
||||
var err error
|
||||
fontContent, err = truetype.Parse(ttfBytes)
|
||||
if err != nil {
|
||||
return nil, ErrFontNotLoaded
|
||||
}
|
||||
}
|
||||
|
||||
img, err := RenderTextToImage(fontContent, text, fontSize, textColor, charSpacing, lineSpacing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 裁剪图像四周的空白
|
||||
return trimImage(img), nil
|
||||
}
|
||||
|
||||
// splitTextIntoLines 将文本按换行符分割成行
|
||||
func splitTextIntoLines(text string) [][]rune {
|
||||
lines := [][]rune{}
|
||||
currentLine := []rune{}
|
||||
|
||||
for _, char := range text {
|
||||
if char == '\n' {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = []rune{}
|
||||
} else {
|
||||
currentLine = append(currentLine, char)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加最后一行
|
||||
if len(currentLine) > 0 {
|
||||
lines = append(lines, currentLine)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// ErrFontNotLoaded 表示字体未加载错误
|
||||
var ErrFontNotLoaded = ErrorMessage("字体未加载,请先调用ParseFont加载字体")
|
||||
|
||||
// ErrorMessage 是一个自定义错误类型
|
||||
type ErrorMessage string
|
||||
|
||||
func (e ErrorMessage) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// createPlaceholderImage 创建替代图像,用于不支持的字符
|
||||
func createPlaceholderImage(fontSize float64, textColor color.Color) *image.RGBA {
|
||||
size := int(fontSize)
|
||||
if size < 10 {
|
||||
size = 10
|
||||
}
|
||||
|
||||
// 创建正方形图像
|
||||
img := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
draw.Draw(img, img.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||||
|
||||
// 绘制边框
|
||||
for x := 0; x < size; x++ {
|
||||
img.Set(x, 0, textColor)
|
||||
img.Set(x, size-1, textColor)
|
||||
}
|
||||
for y := 0; y < size; y++ {
|
||||
img.Set(0, y, textColor)
|
||||
img.Set(size-1, y, textColor)
|
||||
}
|
||||
|
||||
// 绘制对角线
|
||||
for i := 0; i < size; i++ {
|
||||
if i < size {
|
||||
img.Set(i, i, textColor)
|
||||
img.Set(i, size-i-1, textColor)
|
||||
}
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
// drawPlaceholder 在图像上绘制占位符
|
||||
func drawPlaceholder(img *image.RGBA, x, y, size int, c color.Color) {
|
||||
halfSize := size / 2
|
||||
|
||||
// 绘制一个简单的方框
|
||||
for i := 0; i < size; i++ {
|
||||
img.Set(x+i, y-halfSize, c) // 顶边
|
||||
img.Set(x+i, y+halfSize, c) // 底边
|
||||
}
|
||||
|
||||
for i := -halfSize; i <= halfSize; i++ {
|
||||
img.Set(x, y+i, c) // 左边
|
||||
img.Set(x+size-1, y+i, c) // 右边
|
||||
}
|
||||
|
||||
// 绘制对角线
|
||||
for i := 0; i < size; i++ {
|
||||
y1 := y - halfSize + i
|
||||
y2 := y + halfSize - i
|
||||
|
||||
if y1 >= 0 && y1 < img.Bounds().Max.Y &&
|
||||
y2 >= 0 && y2 < img.Bounds().Max.Y {
|
||||
img.Set(x+i, y1, c)
|
||||
img.Set(x+i, y2, c)
|
||||
}
|
||||
}
|
||||
}
|
11
go.mod
11
go.mod
@ -1,3 +1,14 @@
|
||||
module git.lxtend.com/lixiangwuxian/imagedd
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
|
||||
require golang.org/x/image v0.27.0
|
||||
|
||||
require (
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
)
|
||||
|
12
go.sum
Normal file
12
go.sum
Normal file
@ -0,0 +1,12 @@
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
|
||||
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
121
main.go
Normal file
121
main.go
Normal file
@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
|
||||
"git.lxtend.com/lixiangwuxian/imagedd/font2img"
|
||||
"git.lxtend.com/lixiangwuxian/imagedd/sprite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
board := sprite.NewNamedSpriteBoard()
|
||||
{
|
||||
whiteBackground := image.NewRGBA(image.Rect(0, 0, 200, 200))
|
||||
for i := range whiteBackground.Bounds().Dx() {
|
||||
for j := range whiteBackground.Bounds().Dy() {
|
||||
whiteBackground.Set(i, j, color.White)
|
||||
}
|
||||
}
|
||||
board.AddSprite(&sprite.Sprite{
|
||||
Name: "background",
|
||||
Image: whiteBackground,
|
||||
Index: 0,
|
||||
Position: image.Point{X: 0, Y: 0},
|
||||
})
|
||||
}
|
||||
// {
|
||||
// wordSprite := &sprite.Sprite{
|
||||
// Name: "word",
|
||||
// Index: 5,
|
||||
// Position: image.Point{X: 0, Y: 0},
|
||||
// }
|
||||
// img, err := font2img.RenderCharToImage(font, '🤣', 90, color.Black)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// wordSprite.Image = img
|
||||
// board.AddSprite(wordSprite)
|
||||
// }
|
||||
{
|
||||
textSprite := &sprite.Sprite{
|
||||
Name: "text",
|
||||
Index: 5,
|
||||
Position: image.Point{X: 10, Y: 12},
|
||||
}
|
||||
img, err := font2img.RenderTextToTrimmedImage(nil, "usss测试\n测试📧测✌试测\n试🥳🧁🍰\n🎁🎂🎈🎺🎉🎊\n📧🧿🌶无焊无缝🔋😂❤😍🤣😊🥺\n🙏💕😭😘👍\n😅👏测试测试", 12, color.Black, 0, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
textSprite.Image = img
|
||||
board.AddSprite(textSprite)
|
||||
}
|
||||
// {
|
||||
// faceBytes, err := os.ReadFile("face.png")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// faceImage, err := png.Decode(bytes.NewReader(faceBytes))
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// faceSprite := &sprite.Sprite{
|
||||
// Name: "face",
|
||||
// Image: faceImage,
|
||||
// Index: 1,
|
||||
// Position: image.Point{X: -100, Y: -100},
|
||||
// }
|
||||
// faceSprite.Rotate(math.Pi / 2)
|
||||
// board.AddSprite(faceSprite)
|
||||
// }
|
||||
// {
|
||||
// rect := image.NewRGBA(image.Rect(0, 0, 101, 101))
|
||||
// rectSprite := &sprite.Sprite{
|
||||
// Name: "rect",
|
||||
// Image: rect,
|
||||
// Index: 2,
|
||||
// Position: image.Point{X: 20, Y: 20},
|
||||
// }
|
||||
// lineL := &sprite.Line{
|
||||
// Start: image.Point{X: 0, Y: 100},
|
||||
// End: image.Point{X: 50, Y: 0},
|
||||
// Width: 1,
|
||||
// Color: color.RGBA{0, 0, 0, 255},
|
||||
// }
|
||||
// lineL.AddToSprite(rectSprite)
|
||||
// lineR := &sprite.Line{
|
||||
// Start: image.Point{X: 100, Y: 100},
|
||||
// End: image.Point{X: 50, Y: 0},
|
||||
// Width: 1,
|
||||
// Color: color.RGBA{0, 0, 0, 255},
|
||||
// }
|
||||
// lineR.AddToSprite(rectSprite)
|
||||
// lineB := &sprite.Line{
|
||||
// Start: image.Point{X: 0, Y: 100},
|
||||
// End: image.Point{X: 100, Y: 100},
|
||||
// Width: 1,
|
||||
// Color: color.RGBA{0, 0, 0, 255},
|
||||
// }
|
||||
// lineB.AddToSprite(rectSprite)
|
||||
// board.AddSprite(rectSprite)
|
||||
// }
|
||||
// {
|
||||
// circleSprite := &sprite.Sprite{
|
||||
// Name: "circle",
|
||||
// Image: image.NewRGBA(image.Rect(0, 0, 101, 101)),
|
||||
// Index: 3,
|
||||
// Position: image.Point{X: 30, Y: 20},
|
||||
// }
|
||||
// for r := 15; r < 45; r++ {
|
||||
// circle := &sprite.Circle{
|
||||
// Center: image.Point{X: 50, Y: 50},
|
||||
// Radius: r,
|
||||
// Color: color.RGBA{0, 0, 255, 255},
|
||||
// }
|
||||
// circle.AddToSprite(circleSprite)
|
||||
// }
|
||||
// board.AddSprite(circleSprite)
|
||||
// }
|
||||
board.SaveToPng("test.png")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user