"重构Canvas库结构,完善文档和示例,优化绘图功能实现"

This commit is contained in:
kingecg 2025-07-08 23:02:54 +08:00
parent ed591f80b4
commit a07d5940fc
15 changed files with 1105 additions and 3 deletions

154
README.md
View File

@ -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

64
context.go Normal file
View File

@ -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
}

140
draw.go Normal file
View File

@ -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()
}

BIN
example/example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

75
example/main.go Normal file
View File

@ -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")
}

54
geometry.go Normal file
View File

@ -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)
}

5
go.mod
View File

@ -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
)

2
go.sum
View File

@ -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=

143
gradient.go Normal file
View File

@ -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)
}

View File

@ -1,4 +1,4 @@
package canvas
package origin
import (
"image"

79
path.go Normal file
View File

@ -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))
}
}

54
state.go Normal file
View File

@ -0,0 +1,54 @@
package canvas
import "math"
// Save 保存当前状态
func (c *Context) Save() {
// 深拷贝当前状态
copyState := *c.state
c.states = append(c.states, &copyState)
}
// 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}
}

50
style.go Normal file
View File

@ -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
}

160
text.go Normal file
View File

@ -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()
}

126
util.go Normal file
View File

@ -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
}