diff --git a/canvas.go b/canvas.go new file mode 100644 index 0000000..3c09a5f --- /dev/null +++ b/canvas.go @@ -0,0 +1,776 @@ +package canvas + +import ( + "image" + "image/color" + "image/draw" + "math" + "sort" + + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" +) + +// ==================== 核心结构 ==================== + +// 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 +} + +type path struct { + points []image.Point + start image.Point +} + +// ==================== 渐变实现 ==================== + +// Gradient 渐变接口 +type Gradient interface { + At(x, y int) color.Color +} + +// LinearGradient 线性渐变 +type LinearGradient struct { + X0, Y0, X1, Y1 float64 + Stops []GradientStop +} + +type GradientStop struct { + Offset float64 + Color color.Color +} + +func NewLinearGradient(x0, y0, x1, y1 float64) *LinearGradient { + return &LinearGradient{ + X0: x0, Y0: y0, X1: x1, Y1: y1, + Stops: []GradientStop{}, + } +} + +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 + }) +} + +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 +} + +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{}, + } +} + +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 + }) +} + +func (g *RadialGradient) At(x, y int) color.Color { + if len(g.Stops) == 0 { + return color.Transparent + } + + // 计算到焦点的距离 + // dx0, dy0 := float64(x)-g.X0, float64(y)-g.Y0 + dx1, dy1 := float64(x)-g.X1, float64(y)-g.Y1 + // d0 := math.Sqrt(dx0*dx0 + dy0*dy0) + 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) +} + +// ==================== 画布初始化 ==================== + +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{}, + } +} + +func (c *Context) Image() image.Image { + return c.img +} + +// ==================== 状态管理 ==================== + +func (c *Context) Save() { + // 深拷贝当前状态 + copyState := *c.state + c.states = append(c.states, ©State) +} + +func (c *Context) Restore() { + if len(c.states) > 0 { + lastIndex := len(c.states) - 1 + c.state = c.states[lastIndex] + c.states = c.states[:lastIndex] + } +} + +// ==================== 变换操作 ==================== + +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] +} + +func (c *Context) Rotate(angle float64) { + sin, cos := math.Sin(angle), math.Cos(angle) + c.Transform(cos, sin, -sin, cos, 0, 0) +} + +func (c *Context) Scale(sx, sy float64) { + c.Transform(sx, 0, 0, sy, 0, 0) +} + +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 +} + +func (c *Context) SetTransform(a, b, c1, d, e, f float64) { + c.state.transform = [6]float64{a, b, c1, d, e, f} +} + +// ==================== 路径操作 ==================== + +func (c *Context) BeginPath() { + c.path = &path{} +} + +func (c *Context) MoveTo(x, y float64) { + pt := c.transformPoint(x, y) + c.path.points = append(c.path.points, pt) + c.path.start = pt +} + +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)) +} + +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() +} + +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) + } + } +} + +func (c *Context) ClosePath() { + if len(c.path.points) > 0 { + c.LineTo(float64(c.path.start.X), float64(c.path.start.Y)) + } +} + +// ==================== 绘制操作 ==================== + +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) + } +} + +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]) + } +} + +func (c *Context) FillRect(x, y, width, height float64) { + c.BeginPath() + c.Rect(x, y, width, height) + c.Fill() +} + +func (c *Context) StrokeRect(x, y, width, height float64) { + c.BeginPath() + c.Rect(x, y, width, height) + c.Stroke() +} + +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) +} + +// ==================== 文本渲染 ==================== + +// 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) SetFillStyle(style interface{}) { + switch s := style.(type) { + case color.Color, Gradient: + c.state.fillStyle = s + } +} + +func (c *Context) SetStrokeStyle(style interface{}) { + switch s := style.(type) { + case color.Color, Gradient: + c.state.strokeStyle = s + } +} + +func (c *Context) SetFillColor(color color.Color) { + c.state.fillStyle = color +} + +func (c *Context) SetStrokeColor(color color.Color) { + c.state.strokeStyle = color +} + +func (c *Context) SetLineWidth(width float64) { + c.state.lineWidth = width +} + +func (c *Context) SetGlobalAlpha(alpha float64) { + c.state.globalAlpha = math.Max(0, math.Min(1, alpha)) +} + +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 +} + +// ==================== 辅助函数 ==================== + +// 变换点坐标 +func (c *Context) transformPoint(x, y float64) image.Point { + m := c.state.transform + tx := m[0]*x + m[2]*y + m[4] + ty := m[1]*x + m[3]*y + m[5] + return image.Pt(int(tx+0.5), int(ty+0.5)) +} + +// 应用透明度 +func (c *Context) applyAlpha(col color.Color) color.Color { + if c.state.globalAlpha == 1 { + return col + } + r, g, b, a := col.RGBA() + alpha := uint16(float64(a) * c.state.globalAlpha) + return color.RGBA64{ + uint16(r), + uint16(g), + uint16(b), + uint16(alpha), + } +} + +// 颜色插值 +func interpolateColor(c1, c2 color.Color, t float64) color.Color { + r1, g1, b1, a1 := c1.RGBA() + r2, g2, b2, a2 := c2.RGBA() + + return color.RGBA{ + uint8(int(float64(r1>>8)*(1-t) + float64(r2>>8)*t)), + uint8(int(float64(g1>>8)*(1-t) + float64(g2>>8)*t)), + uint8(int(float64(b1>>8)*(1-t) + float64(b2>>8)*t)), + uint8(int(float64(a1>>8)*(1-t) + float64(a2>>8)*t)), + } +} + +// 绘制线段 +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) 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() +} + +// 绘制阴影 +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() +} + +// ==================== 内部类型 ==================== + +// 多边形填充 +type filledPolygon struct{ points []image.Point } + +func (p *filledPolygon) ColorModel() color.Model { + return color.AlphaModel +} + +func (p *filledPolygon) Bounds() image.Rectangle { + return boundingBox(p.points) +} + +func (p *filledPolygon) At(x, y int) color.Color { + if pointInPolygon(image.Pt(x, y), p.points) { + return color.Alpha{255} + } + return color.Alpha{0} +} + +// 渐变图像适配器 +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 { + col := g.grad.At(x, y) + if g.alpha == 1 { + return col + } + r, gVal, b, a := col.RGBA() + newA := uint16(float64(a) * g.alpha) + return color.RGBA64{ + uint16(r), + uint16(gVal), + uint16(b), + newA, + } +} + +// ==================== 几何辅助函数 ==================== + +// 计算多边形边界框 +func boundingBox(points []image.Point) image.Rectangle { + if len(points) == 0 { + return image.Rect(0, 0, 0, 0) + } + + minX, minY := points[0].X, points[0].Y + maxX, maxY := minX, minY + + for _, p := range points { + if p.X < minX { + minX = p.X + } + if p.X > maxX { + maxX = p.X + } + if p.Y < minY { + minY = p.Y + } + if p.Y > maxY { + maxY = p.Y + } + } + + return image.Rect(minX, minY, maxX, maxY) +} + +// 判断点是否在多边形内 +func pointInPolygon(point image.Point, polygon []image.Point) bool { + if len(polygon) < 3 { + return false + } + + intersections := 0 + j := len(polygon) - 1 + + for i := 0; i < len(polygon); i++ { + if (polygon[i].Y > point.Y) != (polygon[j].Y > point.Y) && + point.X < (polygon[j].X-polygon[i].X)*(point.Y-polygon[i].Y)/ + (polygon[j].Y-polygon[i].Y)+polygon[i].X { + intersections++ + } + j = i + } + + return intersections%2 != 0 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b7cfdb0 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.kingecg.top/kingecg/canvas + +go 1.23.1 + +require golang.org/x/image v0.28.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d841c55 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= +golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=