From a07d5940fc26e9b2c56f922574bf83f8f671d470 Mon Sep 17 00:00:00 2001 From: kingecg Date: Tue, 8 Jul 2025 23:02:54 +0800 Subject: [PATCH] =?UTF-8?q?"=E9=87=8D=E6=9E=84Canvas=E5=BA=93=E7=BB=93?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3=E5=92=8C?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=EF=BC=8C=E4=BC=98=E5=8C=96=E7=BB=98=E5=9B=BE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 154 +++++++++++++++++++++++++++++++- context.go | 64 ++++++++++++++ draw.go | 140 +++++++++++++++++++++++++++++ example/example.png | Bin 0 -> 3798 bytes example/main.go | 75 ++++++++++++++++ geometry.go | 54 ++++++++++++ go.mod | 5 +- go.sum | 2 + gradient.go | 143 ++++++++++++++++++++++++++++++ canvas.go => origin/canvas.go | 2 +- path.go | 79 +++++++++++++++++ state.go | 54 ++++++++++++ style.go | 50 +++++++++++ text.go | 160 ++++++++++++++++++++++++++++++++++ util.go | 126 ++++++++++++++++++++++++++ 15 files changed, 1105 insertions(+), 3 deletions(-) create mode 100644 context.go create mode 100644 draw.go create mode 100644 example/example.png create mode 100644 example/main.go create mode 100644 geometry.go create mode 100644 gradient.go rename canvas.go => origin/canvas.go (99%) create mode 100644 path.go create mode 100644 state.go create mode 100644 style.go create mode 100644 text.go create mode 100644 util.go diff --git a/README.md b/README.md index 42f801f..e39c9fc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,154 @@ -# canvas +# Canvas +Canvas 是一个用 Go 语言实现的 2D 绘图库,提供类似于 HTML5 Canvas API 的功能。它允许你在 Go 程序中创建和操作图像,支持基本的绘图操作、变换、渐变和文本渲染。 + +## 功能特性 + +- 基本图形绘制:线条、矩形、圆形、路径 +- 填充和描边操作 +- 线性和径向渐变 +- 文本渲染 +- 变换操作:平移、旋转、缩放 +- 状态保存和恢复 +- 阴影效果 + +## 安装 + +```bash +go get github.com/yourusername/canvas +``` + +## 快速开始 + +以下是一个简单的示例,展示如何使用 Canvas 库创建一个包含矩形、圆形和文本的图像: + +```go +package main + +import ( + "image/color" + "image/png" + "os" + + "github.com/golang/freetype/truetype" + "golang.org/x/image/font/gofont/goregular" + "github.com/yourusername/canvas" +) + +func main() { + // 创建一个300x200的画布 + ctx := canvas.NewContext(300, 200) + + // 设置背景色 + ctx.SetFillColor(color.RGBA{240, 240, 240, 255}) + ctx.FillRect(0, 0, 300, 200) + + // 绘制矩形 + ctx.SetFillColor(color.RGBA{200, 0, 0, 200}) + ctx.FillRect(20, 20, 100, 80) + + // 绘制圆形 + ctx.BeginPath() + ctx.SetFillColor(color.RGBA{0, 0, 200, 200}) + ctx.Arc(200, 60, 40, 0, 2*3.14159) + ctx.Fill() + + // 创建线性渐变 + gradient := canvas.NewLinearGradient(20, 120, 280, 180) + gradient.AddColorStop(0, color.RGBA{255, 0, 0, 255}) + gradient.AddColorStop(0.5, color.RGBA{0, 255, 0, 255}) + gradient.AddColorStop(1, color.RGBA{0, 0, 255, 255}) + + // 使用渐变填充矩形 + ctx.SetFillStyle(gradient) + ctx.FillRect(20, 120, 260, 60) + + // 加载字体 + font, _ := truetype.Parse(goregular.TTF) + face := truetype.NewFace(font, &truetype.Options{Size: 20}) + + // 设置字体和文本属性 + ctx.SetFont(face) + ctx.SetTextAlign("center") + ctx.SetTextBaseline("middle") + ctx.SetFillColor(color.RGBA{0, 0, 0, 255}) + ctx.FillText("Canvas 示例", 150, 30) + + // 保存为PNG图片 + img := ctx.Image() + f, _ := os.Create("example.png") + defer f.Close() + png.Encode(f, img) +} +``` + +## API 参考 + +### 上下文创建 + +- `NewContext(width, height int) *Context` - 创建新的绘图上下文 + +### 路径操作 + +- `BeginPath()` - 开始新路径 +- `MoveTo(x, y float64)` - 移动到指定位置 +- `LineTo(x, y float64)` - 绘制线段到指定位置 +- `Arc(x, y, radius, startAngle, endAngle float64)` - 绘制圆弧 +- `Rect(x, y, width, height float64)` - 绘制矩形路径 +- `ClosePath()` - 闭合路径 + +### 绘制操作 + +- `Fill()` - 填充当前路径 +- `Stroke()` - 描边当前路径 +- `FillRect(x, y, width, height float64)` - 填充矩形 +- `StrokeRect(x, y, width, height float64)` - 描边矩形 +- `ClearRect(x, y, width, height float64)` - 清除矩形区域 + +### 样式设置 + +- `SetFillStyle(style interface{})` - 设置填充样式 +- `SetStrokeStyle(style interface{})` - 设置描边样式 +- `SetFillColor(color color.Color)` - 设置填充颜色 +- `SetStrokeColor(color color.Color)` - 设置描边颜色 +- `SetLineWidth(width float64)` - 设置线宽 +- `SetGlobalAlpha(alpha float64)` - 设置全局透明度 +- `SetShadow(offsetX, offsetY, blur float64, color color.Color)` - 设置阴影 + +### 渐变 + +- `NewLinearGradient(x0, y0, x1, y1 float64) *LinearGradient` - 创建线性渐变 +- `NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) *RadialGradient` - 创建径向渐变 +- `AddColorStop(offset float64, color color.Color)` - 添加渐变色标 + +### 文本操作 + +- `SetFont(face font.Face)` - 设置字体 +- `SetTextAlign(align string)` - 设置文本对齐方式 +- `SetTextBaseline(baseline string)` - 设置文本基线 +- `FillText(text string, x, y float64)` - 绘制填充文本 +- `StrokeText(text string, x, y float64)` - 绘制描边文本 +- `MeasureText(text string) float64` - 测量文本宽度 + +### 变换操作 + +- `Save()` - 保存当前状态 +- `Restore()` - 恢复上一个状态 +- `Translate(x, y float64)` - 平移变换 +- `Rotate(angle float64)` - 旋转变换 +- `Scale(sx, sy float64)` - 缩放变换 +- `Transform(a, b, c, d, e, f float64)` - 应用变换矩阵 +- `SetTransform(a, b, c, d, e, f float64)` - 设置变换矩阵 + +### 图像操作 + +- `Image() *image.RGBA` - 获取底层图像 + +## 依赖 + +- `golang.org/x/image/font` - 用于文本渲染 +- `github.com/golang/freetype/truetype` - 用于字体处理(示例中使用) + +## 许可证 + +MIT \ No newline at end of file diff --git a/context.go b/context.go new file mode 100644 index 0000000..61ad32e --- /dev/null +++ b/context.go @@ -0,0 +1,64 @@ +package canvas + +import ( + "image" + "image/color" + "image/draw" + + "golang.org/x/image/font" +) + +// Context 画布上下文 +type Context struct { + img *image.RGBA // 底层图像 + state *contextState // 当前状态 + states []*contextState // 状态栈 + path *path // 当前路径 +} + +type contextState struct { + fillStyle interface{} // color.Color 或 Gradient + strokeStyle interface{} // color.Color 或 Gradient + lineWidth float64 + globalAlpha float64 + transform [6]float64 // 变换矩阵 [a, b, c, d, e, f] + fontFace font.Face + textAlign string + textBaseline string + shadowColor color.Color + shadowOffsetX float64 + shadowOffsetY float64 + shadowBlur float64 +} + +// NewContext 创建新的画布上下文 +func NewContext(width, height int) *Context { + img := image.NewRGBA(image.Rect(0, 0, width, height)) + // 白色背景 + draw.Draw(img, img.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) + + initialState := &contextState{ + fillStyle: color.Black, + strokeStyle: color.Black, + lineWidth: 1.0, + globalAlpha: 1.0, + transform: [6]float64{1, 0, 0, 1, 0, 0}, // 单位矩阵 + textAlign: "start", + textBaseline: "alphabetic", + shadowColor: color.RGBA{0, 0, 0, 128}, + shadowOffsetX: 0, + shadowOffsetY: 0, + shadowBlur: 0, + } + return &Context{ + img: img, + state: initialState, + states: []*contextState{}, + path: &path{}, + } +} + +// Image 返回底层图像 +func (c *Context) Image() image.Image { + return c.img +} diff --git a/draw.go b/draw.go new file mode 100644 index 0000000..c79157e --- /dev/null +++ b/draw.go @@ -0,0 +1,140 @@ +package canvas + +import ( + "image" + "image/color" + "image/draw" + "math" +) + +// Fill 填充当前路径 +func (c *Context) Fill() { + if len(c.path.points) < 3 { + return + } + + // 应用阴影 + if c.state.shadowBlur > 0 || c.state.shadowOffsetX != 0 || c.state.shadowOffsetY != 0 { + c.drawShadow(true) + } + + // 创建多边形 + poly := make([]image.Point, len(c.path.points)) + for i, p := range c.path.points { + poly[i] = p + } + + // 绘制填充 + switch style := c.state.fillStyle.(type) { + case color.Color: + fillColor := c.applyAlpha(style) + draw.DrawMask(c.img, c.img.Bounds(), + image.NewUniform(fillColor), + image.Point{}, + &filledPolygon{poly}, + image.Point{}, + draw.Over) + case Gradient: + draw.DrawMask(c.img, c.img.Bounds(), + &gradientImage{grad: style, alpha: c.state.globalAlpha}, + image.Point{}, + &filledPolygon{poly}, + image.Point{}, + draw.Over) + } +} + +// Stroke 描边当前路径 +func (c *Context) Stroke() { + if len(c.path.points) < 2 { + return + } + + // 应用阴影 + if c.state.shadowBlur > 0 || c.state.shadowOffsetX != 0 || c.state.shadowOffsetY != 0 { + c.drawShadow(false) + } + + // 绘制线段 + for i := 0; i < len(c.path.points)-1; i++ { + c.drawLine(c.path.points[i], c.path.points[i+1]) + } +} + +// FillRect 填充矩形 +func (c *Context) FillRect(x, y, width, height float64) { + c.BeginPath() + c.Rect(x, y, width, height) + c.Fill() +} + +// StrokeRect 描边矩形 +func (c *Context) StrokeRect(x, y, width, height float64) { + c.BeginPath() + c.Rect(x, y, width, height) + c.Stroke() +} + +// ClearRect 清除矩形区域 +func (c *Context) ClearRect(x, y, width, height float64) { + rect := image.Rect( + int(x), int(y), + int(x+width), int(y+height), + ) + draw.Draw(c.img, rect, image.Transparent, image.Point{}, draw.Src) +} + +// 绘制线段 +func (c *Context) drawLine(p1, p2 image.Point) { + dx := p2.X - p1.X + dy := p2.Y - p1.Y + steps := int(math.Max(math.Abs(float64(dx)), math.Abs(float64(dy)))) + + if steps == 0 { + return + } + + xStep := float64(dx) / float64(steps) + yStep := float64(dy) / float64(steps) + + x := float64(p1.X) + y := float64(p1.Y) + + for i := 0; i <= steps; i++ { + switch style := c.state.strokeStyle.(type) { + case color.Color: + strokeColor := c.applyAlpha(style) + c.img.Set(int(x), int(y), strokeColor) + case Gradient: + gradCol := style.At(int(x), int(y)) + c.img.Set(int(x), int(y), c.applyAlpha(gradCol)) + } + x += xStep + y += yStep + } +} + +// 绘制阴影 +func (c *Context) drawShadow(fill bool) { + // 保存当前状态 + c.Save() + + // 设置阴影样式 + shadowStyle := c.state.shadowColor + c.SetFillColor(shadowStyle) + c.SetStrokeColor(shadowStyle) + c.SetGlobalAlpha(0.5 * c.state.globalAlpha) // 阴影透明度 + + // 应用阴影偏移 + c.Translate(c.state.shadowOffsetX, c.state.shadowOffsetY) + + // 绘制阴影 + if fill { + c.Fill() + } else { + c.Stroke() + } + + // 恢复状态 + c.Restore() +} diff --git a/example/example.png b/example/example.png new file mode 100644 index 0000000000000000000000000000000000000000..207029309b90f39d2768c64adbf064b8ea4d789d GIT binary patch literal 3798 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 literal 0 HcmV?d00001 diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..4d126f7 --- /dev/null +++ b/example/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "image/color" + "image/png" + "log" + "os" + + "github.com/golang/freetype/truetype" + "golang.org/x/image/font/gofont/goregular" + + "git.kingecg.top/kingecg/canvas" // 导入本地canvas包 +) + +func main() { + // 创建一个300x200的画布 + ctx := canvas.NewContext(300, 200) + + // 设置背景色 + ctx.SetFillColor(color.RGBA{240, 240, 240, 255}) + ctx.FillRect(0, 0, 300, 200) + + // 绘制矩形 + ctx.SetFillColor(color.RGBA{200, 0, 0, 200}) + ctx.FillRect(20, 20, 100, 80) + + // 绘制圆形 + ctx.BeginPath() + ctx.SetFillColor(color.RGBA{0, 0, 200, 200}) + ctx.Arc(200, 60, 40, 0, 2*3.14159) + ctx.Fill() + + // 创建线性渐变 + gradient := canvas.NewLinearGradient(20, 120, 280, 180) + gradient.AddColorStop(0, color.RGBA{255, 0, 0, 255}) + gradient.AddColorStop(0.5, color.RGBA{0, 255, 0, 255}) + gradient.AddColorStop(1, color.RGBA{0, 0, 255, 255}) + + // 使用渐变填充矩形 + ctx.SetFillStyle(gradient) + ctx.FillRect(20, 120, 260, 60) + + // 绘制文本 + // 加载字体 + font, err := truetype.Parse(goregular.TTF) + if err != nil { + log.Fatalf("无法解析字体: %v", err) + } + + // 创建字体face + face := truetype.NewFace(font, &truetype.Options{ + Size: 20, + }) + + // 设置字体和文本属性 + ctx.SetFont(face) + ctx.SetTextAlign("center") + ctx.SetTextBaseline("middle") + ctx.SetFillColor(color.RGBA{0, 0, 0, 255}) + ctx.FillText("Canvas 示例", 150, 30) + + // 保存为PNG图片 + img := ctx.Image() + f, err := os.Create("example.png") + if err != nil { + log.Fatalf("无法创建文件: %v", err) + } + defer f.Close() + + if err := png.Encode(f, img); err != nil { + log.Fatalf("无法编码PNG: %v", err) + } + + log.Println("图像已保存为 example.png") +} diff --git a/geometry.go b/geometry.go new file mode 100644 index 0000000..3dacaa9 --- /dev/null +++ b/geometry.go @@ -0,0 +1,54 @@ +package canvas + +import ( + "image" + "math" +) + +// 变换点坐标 +func (c *Context) transformPoint(x, y float64) image.Point { + // 应用变换矩阵 [a, b, c, d, e, f] + // | a c e | | x | + // | b d f | * | y | + // | 0 0 1 | | 1 | + tx := x*c.state.transform[0] + y*c.state.transform[2] + c.state.transform[4] + ty := x*c.state.transform[1] + y*c.state.transform[3] + c.state.transform[5] + return image.Point{X: int(tx), Y: int(ty)} +} + +// 计算两点之间的距离 +func distance(x1, y1, x2, y2 float64) float64 { + dx := x2 - x1 + dy := y2 - y1 + return math.Sqrt(dx*dx + dy*dy) +} + +// 计算贝塞尔曲线点 +func bezierPoint(t float64, p0, p1, p2, p3 float64) float64 { + u := 1 - t + tt := t * t + uu := u * u + uuu := uu * u + ttt := tt * t + + // (1-t)^3 * P0 + 3 * (1-t)^2 * t * P1 + 3 * (1-t) * t^2 * P2 + t^3 * P3 + return uuu*p0 + 3*uu*t*p1 + 3*u*tt*p2 + ttt*p3 +} + +// 计算二次贝塞尔曲线点 +func quadraticPoint(t float64, p0, p1, p2 float64) float64 { + u := 1 - t + return u*u*p0 + 2*u*t*p1 + t*t*p2 +} + +// 计算椭圆上的点 +func ellipsePoint(cx, cy, rx, ry, angle float64) (float64, float64) { + x := cx + rx*math.Cos(angle) + y := cy + ry*math.Sin(angle) + return x, y +} + +// 计算圆上的点 +func circlePoint(cx, cy, r, angle float64) (float64, float64) { + return ellipsePoint(cx, cy, r, r, angle) +} diff --git a/go.mod b/go.mod index b7cfdb0..6248569 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module git.kingecg.top/kingecg/canvas go 1.23.1 -require golang.org/x/image v0.28.0 +require ( + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + golang.org/x/image v0.28.0 +) diff --git a/go.sum b/go.sum index d841c55..6d432e8 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +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= golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= diff --git a/gradient.go b/gradient.go new file mode 100644 index 0000000..3d2f6ae --- /dev/null +++ b/gradient.go @@ -0,0 +1,143 @@ +package canvas + +import ( + "image/color" + "math" + "sort" +) + +// Gradient 渐变接口 +type Gradient interface { + At(x, y int) color.Color +} + +// GradientStop 渐变色标 +type GradientStop struct { + Offset float64 + Color color.Color +} + +// LinearGradient 线性渐变 +type LinearGradient struct { + X0, Y0, X1, Y1 float64 + Stops []GradientStop +} + +// NewLinearGradient 创建线性渐变 +func NewLinearGradient(x0, y0, x1, y1 float64) *LinearGradient { + return &LinearGradient{ + X0: x0, Y0: y0, X1: x1, Y1: y1, + Stops: []GradientStop{}, + } +} + +// AddColorStop 添加色标 +func (g *LinearGradient) AddColorStop(offset float64, color color.Color) { + g.Stops = append(g.Stops, GradientStop{Offset: offset, Color: color}) + sort.Slice(g.Stops, func(i, j int) bool { + return g.Stops[i].Offset < g.Stops[j].Offset + }) +} + +// At 获取指定位置的颜色 +func (g *LinearGradient) At(x, y int) color.Color { + if len(g.Stops) == 0 { + return color.Transparent + } + + // 计算投影位置 (0-1) + dx := g.X1 - g.X0 + dy := g.Y1 - g.Y0 + lenSq := dx*dx + dy*dy + t := 0.0 + + if lenSq > 0 { + t = ((float64(x)-g.X0)*dx + (float64(y)-g.Y0)*dy) / lenSq + if t < 0 { + t = 0 + } else if t > 1 { + t = 1 + } + } + + // 查找色标区间 + var start, end GradientStop + for i, stop := range g.Stops { + if stop.Offset >= t { + if i == 0 { + return stop.Color + } + start = g.Stops[i-1] + end = stop + break + } + } + if start.Color == nil { + return g.Stops[len(g.Stops)-1].Color + } + + // 区间插值 + localT := (t - start.Offset) / (end.Offset - start.Offset) + return interpolateColor(start.Color, end.Color, localT) +} + +// RadialGradient 径向渐变 +type RadialGradient struct { + X0, Y0, R0, X1, Y1, R1 float64 + Stops []GradientStop +} + +// NewRadialGradient 创建径向渐变 +func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) *RadialGradient { + return &RadialGradient{ + X0: x0, Y0: y0, R0: r0, X1: x1, Y1: y1, R1: r1, + Stops: []GradientStop{}, + } +} + +// AddColorStop 添加色标 +func (g *RadialGradient) AddColorStop(offset float64, color color.Color) { + g.Stops = append(g.Stops, GradientStop{Offset: offset, Color: color}) + sort.Slice(g.Stops, func(i, j int) bool { + return g.Stops[i].Offset < g.Stops[j].Offset + }) +} + +// At 获取指定位置的颜色 +func (g *RadialGradient) At(x, y int) color.Color { + if len(g.Stops) == 0 { + return color.Transparent + } + + // 计算到焦点的距离 + dx1, dy1 := float64(x)-g.X1, float64(y)-g.Y1 + d1 := math.Sqrt(dx1*dx1 + dy1*dy1) + + // 计算梯度值 (0-1) + t := (d1 - g.R0) / (g.R1 - g.R0) + if t < 0 { + t = 0 + } else if t > 1 { + t = 1 + } + + // 查找色标区间 + var start, end GradientStop + for i, stop := range g.Stops { + if stop.Offset >= t { + if i == 0 { + return stop.Color + } + start = g.Stops[i-1] + end = stop + break + } + } + if start.Color == nil { + return g.Stops[len(g.Stops)-1].Color + } + + // 区间插值 + localT := (t - start.Offset) / (end.Offset - start.Offset) + return interpolateColor(start.Color, end.Color, localT) +} diff --git a/canvas.go b/origin/canvas.go similarity index 99% rename from canvas.go rename to origin/canvas.go index 3c09a5f..0a6e0eb 100644 --- a/canvas.go +++ b/origin/canvas.go @@ -1,4 +1,4 @@ -package canvas +package origin import ( "image" diff --git a/path.go b/path.go new file mode 100644 index 0000000..dc5b3f6 --- /dev/null +++ b/path.go @@ -0,0 +1,79 @@ +package canvas + +import ( + "image" + "math" +) + +// path 路径结构 +type path struct { + points []image.Point + start image.Point +} + +// BeginPath 开始新路径 +func (c *Context) BeginPath() { + c.path = &path{} +} + +// MoveTo 移动到指定位置 +func (c *Context) MoveTo(x, y float64) { + pt := c.transformPoint(x, y) + c.path.points = append(c.path.points, pt) + c.path.start = pt +} + +// LineTo 绘制线段到指定位置 +func (c *Context) LineTo(x, y float64) { + if len(c.path.points) == 0 { + c.MoveTo(x, y) + return + } + c.path.points = append(c.path.points, c.transformPoint(x, y)) +} + +// Rect 绘制矩形路径 +func (c *Context) Rect(x, y, width, height float64) { + c.MoveTo(x, y) + c.LineTo(x+width, y) + c.LineTo(x+width, y+height) + c.LineTo(x, y+height) + c.ClosePath() +} + +// Arc 绘制圆弧路径 +func (c *Context) Arc(x, y, radius, startAngle, endAngle float64) { + // 确保角度在0-2π之间 + for startAngle < 0 { + startAngle += 2 * math.Pi + } + for endAngle < 0 { + endAngle += 2 * math.Pi + } + + // 确定步数 + angleRange := endAngle - startAngle + if angleRange < 0 { + angleRange += 2 * math.Pi + } + steps := int(math.Ceil(angleRange * 10)) // 每弧度10步 + + for i := 0; i <= steps; i++ { + t := float64(i) / float64(steps) + angle := startAngle + t*angleRange + px := x + radius*math.Cos(angle) + py := y + radius*math.Sin(angle) + if i == 0 { + c.MoveTo(px, py) + } else { + c.LineTo(px, py) + } + } +} + +// ClosePath 闭合路径 +func (c *Context) ClosePath() { + if len(c.path.points) > 0 { + c.LineTo(float64(c.path.start.X), float64(c.path.start.Y)) + } +} diff --git a/state.go b/state.go new file mode 100644 index 0000000..c1ed188 --- /dev/null +++ b/state.go @@ -0,0 +1,54 @@ +package canvas + +import "math" + +// Save 保存当前状态 +func (c *Context) Save() { + // 深拷贝当前状态 + copyState := *c.state + c.states = append(c.states, ©State) +} + +// Restore 恢复上一个状态 +func (c *Context) Restore() { + if len(c.states) > 0 { + lastIndex := len(c.states) - 1 + c.state = c.states[lastIndex] + c.states = c.states[:lastIndex] + } +} + +// Translate 平移变换 +func (c *Context) Translate(x, y float64) { + c.state.transform[4] += x*c.state.transform[0] + y*c.state.transform[2] + c.state.transform[5] += x*c.state.transform[1] + y*c.state.transform[3] +} + +// Rotate 旋转变换 +func (c *Context) Rotate(angle float64) { + sin, cos := math.Sin(angle), math.Cos(angle) + c.Transform(cos, sin, -sin, cos, 0, 0) +} + +// Scale 缩放变换 +func (c *Context) Scale(sx, sy float64) { + c.Transform(sx, 0, 0, sy, 0, 0) +} + +// Transform 应用变换矩阵 +func (c *Context) Transform(a, b, cVal, d, e, f float64) { + newMatrix := [6]float64{ + a*c.state.transform[0] + cVal*c.state.transform[1], + b*c.state.transform[0] + d*c.state.transform[1], + a*c.state.transform[2] + cVal*c.state.transform[3], + b*c.state.transform[2] + d*c.state.transform[3], + a*c.state.transform[4] + cVal*c.state.transform[5] + e, + b*c.state.transform[4] + d*c.state.transform[5] + f, + } + c.state.transform = newMatrix +} + +// SetTransform 设置变换矩阵 +func (c *Context) SetTransform(a, b, c1, d, e, f float64) { + c.state.transform = [6]float64{a, b, c1, d, e, f} +} diff --git a/style.go b/style.go new file mode 100644 index 0000000..8a36200 --- /dev/null +++ b/style.go @@ -0,0 +1,50 @@ +package canvas + +import ( + "image/color" + "math" +) + +// SetFillStyle 设置填充样式 +func (c *Context) SetFillStyle(style interface{}) { + switch s := style.(type) { + case color.Color, Gradient: + c.state.fillStyle = s + } +} + +// SetStrokeStyle 设置描边样式 +func (c *Context) SetStrokeStyle(style interface{}) { + switch s := style.(type) { + case color.Color, Gradient: + c.state.strokeStyle = s + } +} + +// SetFillColor 设置填充颜色 +func (c *Context) SetFillColor(color color.Color) { + c.state.fillStyle = color +} + +// SetStrokeColor 设置描边颜色 +func (c *Context) SetStrokeColor(color color.Color) { + c.state.strokeStyle = color +} + +// SetLineWidth 设置线宽 +func (c *Context) SetLineWidth(width float64) { + c.state.lineWidth = width +} + +// SetGlobalAlpha 设置全局透明度 +func (c *Context) SetGlobalAlpha(alpha float64) { + c.state.globalAlpha = math.Max(0, math.Min(1, alpha)) +} + +// SetShadow 设置阴影 +func (c *Context) SetShadow(offsetX, offsetY, blur float64, color color.Color) { + c.state.shadowOffsetX = offsetX + c.state.shadowOffsetY = offsetY + c.state.shadowBlur = blur + c.state.shadowColor = color +} diff --git a/text.go b/text.go new file mode 100644 index 0000000..32560d8 --- /dev/null +++ b/text.go @@ -0,0 +1,160 @@ +package canvas + +import ( + "image" + "image/color" + + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" +) + +// SetFont 设置字体 +func (c *Context) SetFont(face font.Face) { + c.state.fontFace = face +} + +// SetTextAlign 设置文本对齐 +func (c *Context) SetTextAlign(align string) { + c.state.textAlign = align +} + +// SetTextBaseline 设置文本基线 +func (c *Context) SetTextBaseline(baseline string) { + c.state.textBaseline = baseline +} + +// FillText 绘制填充文本 +func (c *Context) FillText(text string, x, y float64) { + if c.state.fontFace == nil { + return + } + + // 应用阴影 + if c.state.shadowBlur > 0 || c.state.shadowOffsetX != 0 || c.state.shadowOffsetY != 0 { + c.drawTextShadow(text, x, y, true) + } + + c.drawText(text, x, y, true) +} + +// StrokeText 绘制描边文本 +func (c *Context) StrokeText(text string, x, y float64) { + if c.state.fontFace == nil { + return + } + + // 应用阴影 + if c.state.shadowBlur > 0 || c.state.shadowOffsetX != 0 || c.state.shadowOffsetY != 0 { + c.drawTextShadow(text, x, y, false) + } + + c.drawText(text, x, y, false) +} + +// MeasureText 测量文本宽度 +func (c *Context) MeasureText(text string) float64 { + if c.state.fontFace == nil { + return 0 + } + + width := 0 + for _, r := range text { + aw, _ := c.state.fontFace.GlyphAdvance(r) + width += aw.Round() + } + return float64(width) +} + +// 绘制文本 +func (c *Context) drawText(text string, x, y float64, fill bool) { + pt := c.transformPoint(x, y) + + // 计算文本宽度用于对齐 + textWidth := c.MeasureText(text) + + // 应用对齐 + switch c.state.textAlign { + case "center": + pt.X -= int(textWidth / 2) + case "end", "right": + pt.X -= int(textWidth) + } + + // 应用基线 + metrics := c.state.fontFace.Metrics() + switch c.state.textBaseline { + case "top": + pt.Y += metrics.Ascent.Round() + case "middle": + pt.Y += (metrics.Ascent + metrics.Descent).Round() / 2 + case "bottom": + pt.Y += metrics.Descent.Round() + } + + // 创建文本绘制器 + drawer := font.Drawer{ + Dst: c.img, + Face: c.state.fontFace, + Dot: fixed.P(pt.X, pt.Y), + } + + // 设置颜色 + if fill { + switch style := c.state.fillStyle.(type) { + case color.Color: + drawer.Src = image.NewUniform(c.applyAlpha(style)) + case Gradient: + drawer.Src = &gradientImage{grad: style, alpha: c.state.globalAlpha} + } + } else { + // 描边文本 - 简单实现,实际应使用路径 + switch style := c.state.strokeStyle.(type) { + case color.Color: + drawer.Src = image.NewUniform(c.applyAlpha(style)) + case Gradient: + drawer.Src = &gradientImage{grad: style, alpha: c.state.globalAlpha} + } + } + + // 绘制文本 + if fill { + drawer.DrawString(text) + } else { + // 简单描边实现 - 实际应用中应使用更高级的路径方法 + offset := 1 + for dx := -offset; dx <= offset; dx++ { + for dy := -offset; dy <= offset; dy++ { + if dx != 0 || dy != 0 { + drawer.Dot = fixed.P(pt.X+dx, pt.Y+dy) + drawer.DrawString(text) + } + } + } + } +} + +// 绘制文本阴影 +func (c *Context) drawTextShadow(text string, x, y float64, fill bool) { + // 保存当前状态 + c.Save() + + // 设置阴影样式 + shadowStyle := c.state.shadowColor + c.SetFillColor(shadowStyle) + c.SetStrokeColor(shadowStyle) + c.SetGlobalAlpha(0.5 * c.state.globalAlpha) // 阴影透明度 + + // 应用阴影偏移 + x += c.state.shadowOffsetX + y += c.state.shadowOffsetY + + // 绘制阴影文本 + if fill { + c.FillText(text, x, y) + } else { + c.StrokeText(text, x, y) + } + + // 恢复状态 + c.Restore() +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..2f93e95 --- /dev/null +++ b/util.go @@ -0,0 +1,126 @@ +package canvas + +import ( + "image" + "image/color" +) + +// 应用全局透明度 +func (c *Context) applyAlpha(col color.Color) color.Color { + if c.state.globalAlpha >= 1.0 { + return col + } + + r, g, b, a := col.RGBA() + a = uint32(float64(a) * c.state.globalAlpha) + return color.NRGBA64{ + R: uint16(r), + G: uint16(g), + B: uint16(b), + A: uint16(a), + } +} + +// 颜色插值 +func interpolateColor(c1, c2 color.Color, t float64) color.Color { + r1, g1, b1, a1 := c1.RGBA() + r2, g2, b2, a2 := c2.RGBA() + + return color.RGBA64{ + R: uint16(float64(r1)*(1-t) + float64(r2)*t), + G: uint16(float64(g1)*(1-t) + float64(g2)*t), + B: uint16(float64(b1)*(1-t) + float64(b2)*t), + A: uint16(float64(a1)*(1-t) + float64(a2)*t), + } +} + +// 渐变图像 +type gradientImage struct { + grad Gradient + alpha float64 +} + +func (g *gradientImage) ColorModel() color.Model { + return color.RGBAModel +} + +func (g *gradientImage) Bounds() image.Rectangle { + return image.Rect(-1e9, -1e9, 1e9, 1e9) +} + +func (g *gradientImage) At(x, y int) color.Color { + c := g.grad.At(x, y) + if g.alpha < 1.0 { + r, gr, b, a := c.RGBA() + a = uint32(float64(a) * g.alpha) + return color.NRGBA64{ + R: uint16(r), + G: uint16(gr), + B: uint16(b), + A: uint16(a), + } + } + return c +} + +// 填充多边形 +type filledPolygon struct { + points []image.Point +} + +func (p *filledPolygon) ColorModel() color.Model { + return color.AlphaModel +} + +func (p *filledPolygon) Bounds() image.Rectangle { + if len(p.points) == 0 { + return image.Rectangle{} + } + + minX, minY := p.points[0].X, p.points[0].Y + maxX, maxY := minX, minY + + for _, pt := range p.points { + if pt.X < minX { + minX = pt.X + } + if pt.X > maxX { + maxX = pt.X + } + if pt.Y < minY { + minY = pt.Y + } + if pt.Y > maxY { + maxY = pt.Y + } + } + + return image.Rect(minX, minY, maxX+1, maxY+1) +} + +func (p *filledPolygon) At(x, y int) color.Color { + if pointInPolygon(image.Pt(x, y), p.points) { + return color.Alpha{A: 255} + } + return color.Alpha{A: 0} +} + +// 判断点是否在多边形内 +func pointInPolygon(pt image.Point, poly []image.Point) bool { + if len(poly) < 3 { + return false + } + + inside := false + j := len(poly) - 1 + + for i := 0; i < len(poly); i++ { + if (poly[i].Y > pt.Y) != (poly[j].Y > pt.Y) && + (pt.X < poly[i].X+(poly[j].X-poly[i].X)*(pt.Y-poly[i].Y)/(poly[j].Y-poly[i].Y)) { + inside = !inside + } + j = i + } + + return inside +}