refactor: 移除旧的绘制模块,添加新的绘制辅助函数和精灵管理功能,增强图形处理能力。

This commit is contained in:
lixiangwuxian
2025-05-10 02:31:02 +08:00
parent e3149f6079
commit 6cd5efa192
9 changed files with 610 additions and 75 deletions

View File

@@ -1,28 +0,0 @@
package draw
import (
"image"
"image/draw"
)
type Drawer interface {
Draw(img *image.Image)
}
type BaseBoard struct {
Image *image.RGBA64
}
func (b *BaseBoard) SetBaseImage(img *image.Image) {
b.Image = ToRGBA64(img)
}
func (b *BaseBoard) Draw(img *image.Image, pos image.Point) {
draw.Draw(b.Image, b.Image.Bounds(), *img, pos, draw.Over)
}
func ToRGBA64(img *image.Image) *image.RGBA64 {
rgba := image.NewRGBA64((*img).Bounds())
draw.Draw(rgba, rgba.Bounds(), *img, image.Point{}, draw.Over)
return rgba
}

156
drawhelper/draw.go Normal file
View File

@@ -0,0 +1,156 @@
package drawhelper
import (
"image"
"image/color"
"log"
"git.lxtend.com/lixiangwuxian/imagedd/util"
)
// 辅助函数:在图像上绘制宽线条
func DrawThickLineOnImage(img *image.RGBA, x0, y0, x1, y1, width int, color color.Color) {
// 获取颜色分量
r, g, b, a := color.RGBA()
r8, g8, b8, a8 := uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8)
bounds := img.Bounds()
// 使用Bresenham算法绘制线段作为中心线
dx := util.Abs(x1 - x0)
dy := util.Abs(y1 - y0)
sx, sy := 1, 1
if x0 >= x1 {
sx = -1
}
if y0 >= y1 {
sy = -1
}
err := dx - dy
// 计算线宽的一半(向下取整)
halfWidth := width / 2
// 绘制线条
x, y := x0, y0
for {
// 绘制宽线条(在中心点周围绘制圆形区域)
for wy := -halfWidth; wy <= halfWidth; wy++ {
for wx := -halfWidth; wx <= halfWidth; wx++ {
// 只绘制在线宽范围内的点(圆形区域)
if wx*wx+wy*wy <= halfWidth*halfWidth {
DrawPointIfInBounds(img, bounds, x+wx, y+wy, r8, g8, b8, a8)
}
}
}
if x == x1 && y == y1 {
break
}
e2 := 2 * err
if e2 > -dy {
err -= dy
x += sx
}
if e2 < dx {
err += dx
y += sy
}
}
}
// 辅助函数:在图像上绘制线段
func DrawLineOnImage(img *image.RGBA, x0, y0, x1, y1 int, color color.Color) {
r, g, b, a := color.RGBA()
r8, g8, b8, a8 := uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8)
// 输出颜色值用于调试
log.Printf("绘制线条: 颜色 RGBA(%d,%d,%d,%d)", r8, g8, b8, a8)
bounds := img.Bounds()
// 使用Bresenham算法绘制线段
dx := util.Abs(x1 - x0)
dy := util.Abs(y1 - y0)
sx, sy := 1, 1
if x0 >= x1 {
sx = -1
}
if y0 >= y1 {
sy = -1
}
err := dx - dy
for {
// 绘制点,但只在有效范围内
DrawPointIfInBounds(img, bounds, x0, y0, r8, g8, b8, a8)
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 > -dy {
err -= dy
x0 += sx
}
if e2 < dx {
err += dx
y0 += sy
}
}
}
// 辅助函数:在图像上绘制圆
func DrawCircleOnImage(img *image.RGBA, x0, y0, radius int, color color.Color) {
r, g, b, a := color.RGBA()
r8, g8, b8, a8 := uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8)
// 使用Bresenham算法绘制圆
x := 0
y := radius
d := 3 - 2*radius
bounds := img.Bounds()
for {
// 绘制8个对称点但只在图像范围内
DrawPointIfInBounds(img, bounds, x0+x, y0+y, r8, g8, b8, a8)
DrawPointIfInBounds(img, bounds, x0+x, y0-y, r8, g8, b8, a8)
DrawPointIfInBounds(img, bounds, x0-x, y0+y, r8, g8, b8, a8)
DrawPointIfInBounds(img, bounds, x0-x, y0-y, r8, g8, b8, a8)
DrawPointIfInBounds(img, bounds, x0+y, y0+x, r8, g8, b8, a8)
DrawPointIfInBounds(img, bounds, x0+y, y0-x, r8, g8, b8, a8)
DrawPointIfInBounds(img, bounds, x0-y, y0+x, r8, g8, b8, a8)
DrawPointIfInBounds(img, bounds, x0-y, y0-x, r8, g8, b8, a8)
if d < 0 {
d += 4*x + 6
} else {
d += 4*(x-y) + 10
y--
}
x++
if x > y {
break
}
}
}
// 辅助函数:在图像上设置像素,但只在有效范围内
func DrawPointIfInBounds(img *image.RGBA, bounds image.Rectangle, x, y int, r, g, b, a uint8) {
if x >= bounds.Min.X && x < bounds.Max.X && y >= bounds.Min.Y && y < bounds.Max.Y {
// 记录绘制的像素位置和颜色,便于调试
if (r > 0 || g > 0 || b > 0) && a > 0 {
log.Printf("绘制像素: (%d,%d) RGBA(%d,%d,%d,%d)", x, y, r, g, b, a)
}
idx := (y-bounds.Min.Y)*img.Stride + (x-bounds.Min.X)*4
img.Pix[idx] = r
img.Pix[idx+1] = g
img.Pix[idx+2] = b
img.Pix[idx+3] = a
}
}

View File

@@ -1,41 +0,0 @@
package model
import (
"image"
"image/color"
"image/draw"
)
type Line struct {
Start image.Point
End image.Point
Width int
Color color.Color
}
func (l *Line) ToSprite() *Sprite {
img := image.NewRGBA(image.Rect(0, 0, l.End.X-l.Start.X, l.End.Y-l.Start.Y))
draw.Draw(img, image.Rect(0, 0, l.End.X-l.Start.X, l.End.Y-l.Start.Y), &image.Uniform{l.Color}, image.Point{}, draw.Src)
return &Sprite{
Position: l.Start,
Image: img,
}
}
type ProjectMatrix struct {
Matrix [2][2]float64
}
func (p *ProjectMatrix) ProjectPoint(point image.Point) image.Point {
return image.Point{
X: int(p.Matrix[0][0]*float64(point.X) + p.Matrix[0][1]*float64(point.Y)),
Y: int(p.Matrix[1][0]*float64(point.X) + p.Matrix[1][1]*float64(point.Y)),
}
}
func (p *ProjectMatrix) ProjectLine(line Line) Line {
return Line{
Start: p.ProjectPoint(line.Start),
End: p.ProjectPoint(line.End),
}
}

134
sprite/2d.go Normal file
View File

@@ -0,0 +1,134 @@
package sprite
import (
"image"
"image/color"
"image/draw"
"log"
"git.lxtend.com/lixiangwuxian/imagedd/drawhelper"
)
type Line struct {
Start image.Point
End image.Point
Width int
Color color.Color
}
// AddToSprite 在精灵图上绘制线条,保留原有的图像
func (l *Line) AddToSprite(sprite *Sprite) {
log.Printf("开始绘制线条: 起点(%d,%d) 终点(%d,%d) 宽度%d",
l.Start.X, l.Start.Y, l.End.X, l.End.Y, l.Width)
// 检查精灵是否已有图像
if sprite.Image == nil {
// 如果没有图像,创建一个新图像
width := l.End.X - l.Start.X + 1
height := l.End.Y - l.Start.Y + 1
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
img := image.NewRGBA(image.Rect(0, 0, width, height))
sprite.Image = img
sprite.Position = l.Start
log.Printf("创建新图像: 大小(%d,%d) 位置(%d,%d)",
width, height, sprite.Position.X, sprite.Position.Y)
}
// 获取当前图像大小和位置
bounds := sprite.Image.Bounds()
width, height := bounds.Dx(), bounds.Dy()
log.Printf("当前图像: 大小(%d,%d) 位置(%d,%d)",
width, height, sprite.Position.X, sprite.Position.Y)
// 直接使用原图像,不再扩展
var newImage *image.RGBA
newImage, _ = sprite.Image.(*image.RGBA)
if newImage == nil {
// 如果原图像不是RGBA转换为RGBA
newImage = image.NewRGBA(bounds)
draw.Draw(newImage, bounds, sprite.Image, bounds.Min, draw.Src)
log.Printf("转换图像为RGBA格式")
}
// 在图像上绘制线条,根据线宽决定使用哪个函数
// 超出图像边界的部分将被自动丢弃AddToSprite
if l.Width <= 1 {
// 使用标准线条绘制算法
drawhelper.DrawLineOnImage(newImage, l.Start.X, l.Start.Y, l.End.X, l.End.Y, l.Color)
} else {
// 使用粗线条绘制算法
drawhelper.DrawThickLineOnImage(newImage, l.Start.X, l.Start.Y, l.End.X, l.End.Y, l.Width, l.Color)
}
// 更新精灵图像
sprite.Image = newImage
log.Printf("线条绘制完成")
}
type Circle struct {
Center image.Point
Radius int
Color color.Color
}
// ToSprite 在精灵图上绘制圆形,保留原有的图像
func (c *Circle) ToSprite(sprite *Sprite) {
// 检查精灵是否已有图像
if sprite.Image == nil {
// 如果没有图像,创建一个新图像
size := c.Radius * 2
img := image.NewRGBA(image.Rect(0, 0, size, size))
sprite.Image = img
// 设置精灵位置为圆心减去半径
sprite.Position = image.Point{
X: c.Center.X - c.Radius,
Y: c.Center.Y - c.Radius,
}
}
// 获取当前图像大小和位置
bounds := sprite.Image.Bounds()
// 计算圆心相对于精灵图像的位置
relCenterX := c.Center.X - sprite.Position.X
relCenterY := c.Center.Y - sprite.Position.Y
// 直接使用原图像,不再扩展
var newImage *image.RGBA
newImage, _ = sprite.Image.(*image.RGBA)
if newImage == nil {
// 如果原图像不是RGBA转换为RGBA
newImage = image.NewRGBA(bounds)
draw.Draw(newImage, bounds, sprite.Image, bounds.Min, draw.Src)
}
// 在新图像上绘制圆
// 超出图像边界的部分将被自动丢弃
drawhelper.DrawCircleOnImage(newImage, relCenterX, relCenterY, c.Radius, c.Color)
// 更新精灵图像
sprite.Image = newImage
}
type ProjectMatrix struct {
Matrix [2][2]float64
}
func (p *ProjectMatrix) ProjectPoint(point image.Point) image.Point {
return image.Point{
X: int(p.Matrix[0][0]*float64(point.X) + p.Matrix[0][1]*float64(point.Y)),
Y: int(p.Matrix[1][0]*float64(point.X) + p.Matrix[1][1]*float64(point.Y)),
}
}
func (p *ProjectMatrix) ProjectLine(line Line) Line {
return Line{
Start: p.ProjectPoint(line.Start),
End: p.ProjectPoint(line.End),
}
}

284
sprite/2d_test.go Normal file
View File

@@ -0,0 +1,284 @@
package sprite
import (
"image"
"image/color"
"testing"
)
// 测试在精灵上绘制线条
func TestLineToSprite(t *testing.T) {
// 创建一个新的空白精灵
sprite := &Sprite{
Name: "test_line",
Position: image.Point{X: 10, Y: 10},
Index: 1,
}
// 定义一条线
line := &Line{
Start: image.Point{X: 10, Y: 10}, // 起点设置为与精灵位置相同
End: image.Point{X: 50, Y: 30},
Width: 5, // 增加线宽以便更容易找到
Color: color.RGBA{R: 255, G: 0, B: 0, A: 255}, // 红色
}
// 在精灵上绘制线
line.AddToSprite(sprite)
// 验证精灵现在有了图像
if sprite.Image == nil {
t.Fatalf("精灵图像为空")
}
// 验证精灵位置未变(因为线条在精灵边界内)
if sprite.Position.X != 10 || sprite.Position.Y != 10 {
t.Errorf("精灵位置错误:期望(10,10),实际(%d,%d)",
sprite.Position.X, sprite.Position.Y)
}
// 检查图像大小
bounds := sprite.Image.Bounds()
t.Logf("图像大小:%dx%d", bounds.Dx(), bounds.Dy())
if bounds.Dx() < 41 || bounds.Dy() < 21 {
t.Errorf("图像大小错误:期望至少(41x21),实际(%dx%d)",
bounds.Dx(), bounds.Dy())
}
// 检查图像中是否有红色像素
redPixelFound := false
// 扫描整个图像查找红色像素
var redPixels []image.Point
for y := 0; y < bounds.Dy(); y++ {
for x := 0; x < bounds.Dx(); x++ {
pixelColor := sprite.Image.At(x, y)
r, g, b, _ := pixelColor.RGBA()
if r>>8 > 200 && g>>8 < 50 && b>>8 < 50 {
redPixelFound = true
redPixels = append(redPixels, image.Point{X: x, Y: y})
if len(redPixels) >= 5 { // 找到5个点就足够了
break
}
}
}
if len(redPixels) >= 5 {
break
}
}
// 打印找到的红色像素位置
if len(redPixels) > 0 {
t.Logf("找到 %d 个红色像素点", len(redPixels))
for i, p := range redPixels {
t.Logf(" 红色像素点 #%d: (%d,%d)", i+1, p.X, p.Y)
}
}
if !redPixelFound {
// 如果没找到,保存图像用于调试
board := NewNamedSpriteBoard(100, 100)
board.AddSprite(sprite)
board.SaveToPng("test_line1.png")
t.Errorf("未找到红色线条像素")
}
// 保存初始图像大小用于后续比较
initialWidth := bounds.Dx()
initialHeight := bounds.Dy()
// 再绘制一条线(线条一部分超出当前图像边界,应导致图像扩展)
line2 := &Line{
Start: image.Point{X: 5, Y: 20},
End: image.Point{X: 65, Y: 40},
Width: 5, // 增加线宽
Color: color.RGBA{R: 0, G: 0, B: 255, A: 255}, // 蓝色
}
// 使用Sprite.DrawLine方法
sprite.DrawLine(line2)
// 检查图像是否扩展以容纳新线
newBounds := sprite.Image.Bounds()
t.Logf("扩展后图像大小:%dx%d", newBounds.Dx(), newBounds.Dy())
if newBounds.Dx() <= initialWidth || newBounds.Dy() <= initialHeight {
t.Errorf("图像未扩展:原始(%dx%d),现在(%dx%d)",
initialWidth, initialHeight, newBounds.Dx(), newBounds.Dy())
}
// 检查图像中是否有蓝色像素
bluePixelFound := false
// 扫描整个图像查找蓝色像素
var bluePixels []image.Point
for y := 0; y < newBounds.Dy(); y++ {
for x := 0; x < newBounds.Dx(); x++ {
pixelColor := sprite.Image.At(x, y)
r, g, b, _ := pixelColor.RGBA()
if r>>8 < 50 && g>>8 < 50 && b>>8 > 200 {
bluePixelFound = true
bluePixels = append(bluePixels, image.Point{X: x, Y: y})
if len(bluePixels) >= 5 { // 找到5个点就足够了
break
}
}
}
if len(bluePixels) >= 5 {
break
}
}
// 打印找到的蓝色像素位置
if len(bluePixels) > 0 {
t.Logf("找到 %d 个蓝色像素点", len(bluePixels))
for i, p := range bluePixels {
t.Logf(" 蓝色像素点 #%d: (%d,%d)", i+1, p.X, p.Y)
}
}
if !bluePixelFound {
// 保存图像用于调试
board := NewNamedSpriteBoard(100, 100)
board.AddSprite(sprite)
board.SaveToPng("test_line2.png")
t.Errorf("未找到蓝色线条像素")
}
// 总是保存最终图像以便查看结果
board := NewNamedSpriteBoard(100, 100)
board.AddSprite(sprite)
board.SaveToPng("test_line_final.png")
}
// 测试在精灵上绘制圆形
func TestCircleToSprite(t *testing.T) {
// 创建一个新的空白精灵
sprite := &Sprite{
Name: "test_circle",
Position: image.Point{X: 20, Y: 20},
Index: 1,
}
// 定义一个圆
circle := &Circle{
Center: image.Point{X: 30, Y: 30},
Radius: 10,
Color: color.RGBA{R: 0, G: 255, B: 0, A: 255}, // 绿色
}
// 在精灵上绘制圆
circle.ToSprite(sprite)
// 验证精灵现在有了图像
if sprite.Image == nil {
t.Fatalf("精灵图像为空")
}
// 验证精灵位置正确更新
if sprite.Position.X != 20 || sprite.Position.Y != 20 {
t.Errorf("精灵位置错误:期望(20,20),实际(%d,%d)",
sprite.Position.X, sprite.Position.Y)
}
// 检查图像大小
bounds := sprite.Image.Bounds()
t.Logf("图像大小:%dx%d", bounds.Dx(), bounds.Dy())
// 圆的相对坐标是 (30-20, 30-20) 即 (10,10) 中心
// 记录找到绿色像素的位置,用于调试
foundPoints := make([]image.Point, 0)
// 在图像中寻找绿色像素
for y := 0; y < bounds.Dy(); y++ {
for x := 0; x < bounds.Dx(); x++ {
pixelColor := sprite.Image.At(x, y)
r, g, b, _ := pixelColor.RGBA()
if r>>8 < 50 && g>>8 > 200 && b>>8 < 50 {
foundPoints = append(foundPoints, image.Point{X: x, Y: y})
if len(foundPoints) > 10 {
break // 找到足够多的点了
}
}
}
if len(foundPoints) > 10 {
break
}
}
// 如果找到了绿色像素,则测试通过
if len(foundPoints) == 0 {
// 保存图像用于调试
board := NewNamedSpriteBoard(100, 100)
board.AddSprite(sprite)
board.SaveToPng("test_circle1.png")
t.Errorf("未找到绿色圆形像素")
} else {
t.Logf("找到 %d 个绿色像素点", len(foundPoints))
for i, p := range foundPoints {
if i < 5 { // 只打印前5个点
t.Logf(" 绿色像素点 #%d: (%d,%d)", i+1, p.X, p.Y)
}
}
}
// 保存初始图像大小用于后续比较
initialWidth := bounds.Dx()
initialHeight := bounds.Dy()
// 再绘制一个圆(部分在图像边界外,应导致图像扩展)
circle2 := &Circle{
Center: image.Point{X: 15, Y: 15},
Radius: 15,
Color: color.RGBA{R: 255, G: 255, B: 0, A: 255}, // 黄色
}
// 在已有精灵上绘制第二个圆
circle2.ToSprite(sprite)
// 检查图像是否扩展
newBounds := sprite.Image.Bounds()
t.Logf("扩展后图像大小:%dx%d", newBounds.Dx(), newBounds.Dy())
// 检查图像是否扩展了
if newBounds.Dx() <= initialWidth || newBounds.Dy() <= initialHeight {
t.Errorf("图像未正确扩展:原始(%dx%d),现在(%dx%d)",
initialWidth, initialHeight, newBounds.Dx(), newBounds.Dy())
}
// 查找黄色像素
yellowPixelFound := false
yellowPoints := make([]image.Point, 0)
// 在图像中寻找黄色像素
for y := 0; y < newBounds.Dy(); y++ {
for x := 0; x < newBounds.Dx(); x++ {
pixelColor := sprite.Image.At(x, y)
r, g, b, _ := pixelColor.RGBA()
if r>>8 > 200 && g>>8 > 200 && b>>8 < 50 {
yellowPixelFound = true
yellowPoints = append(yellowPoints, image.Point{X: x, Y: y})
if len(yellowPoints) > 10 {
break // 找到足够多的点了
}
}
}
if len(yellowPoints) > 10 {
break
}
}
if !yellowPixelFound {
// 保存图像用于调试
board := NewNamedSpriteBoard(100, 100)
board.AddSprite(sprite)
board.SaveToPng("test_circle2.png")
t.Errorf("未找到黄色圆形像素")
} else {
t.Logf("找到 %d 个黄色像素点", len(yellowPoints))
for i, p := range yellowPoints {
if i < 5 { // 只打印前5个点
t.Logf(" 黄色像素点 #%d: (%d,%d)", i+1, p.X, p.Y)
}
}
}
}

View File

@@ -1,4 +1,4 @@
package model
package sprite
import (
"image"
@@ -501,8 +501,8 @@ func (b *NamedSpriteBoard) RenderToImage() *image.RGBA {
return img
}
// SaveToFile 将精灵板渲染为图像并保存到文件
func (b *NamedSpriteBoard) SaveToFile(filename string) error {
// SaveToPng 将精灵板渲染为图像并保存到文件
func (b *NamedSpriteBoard) SaveToPng(filename string) error {
// 渲染图像
img := b.RenderToImage()

View File

@@ -1,4 +1,4 @@
package model
package sprite
import (
"image"
@@ -333,7 +333,7 @@ func TestNamedSpriteBoardRenderToImage(t *testing.T) {
// 将saveImage设置为true查看实际渲染效果以调试
saveImage := false
if saveImage {
err := board.SaveToFile("test_render.png")
err := board.SaveToPng("test_render.png")
if err != nil {
t.Errorf("保存图像失败: %v", err)
}

View File

@@ -1,4 +1,4 @@
package model
package sprite
import (
"image"
@@ -199,6 +199,11 @@ func (s *Sprite) Project(projectMatrix *ProjectMatrix) {
s.Image = newImage
}
func (s *Sprite) DrawLine(line *Line) {
// 使用Line.ToSprite方法在当前精灵上绘制线条
line.AddToSprite(s)
}
// getImageBytes 尝试提取图像的原始字节数据以加速访问
func getImageBytes(img image.Image) (*image.RGBA, []uint8, bool) {
if rgba, ok := img.(*image.RGBA); ok {

25
util/math.go Normal file
View File

@@ -0,0 +1,25 @@
package util
// 辅助函数:绝对值
func Abs(x int) int {
if x < 0 {
return -x
}
return x
}
// 辅助函数:最小值
func Min(a, b int) int {
if a < b {
return a
}
return b
}
// 辅助函数:最大值
func Max(a, b int) int {
if a > b {
return a
}
return b
}