feat(渲染能力): 支持轴向重复渐变,修正多子路径填充,保持SVG/PDF矢量输出
build.yaml / build (push) Successful in 38s

This commit is contained in:
xiaoqidun committed 2026-09-09 22:22:40 +08:00
1 parent 537b5779e1
commit bed08b2c4b
2 files changed
+75 -3

No files matched your search

+10 -3
View File
@@ -223,6 +223,14 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
fillPaint, shadingClip, fillView := resolveShdPaint(ctx, style.fillPaint) fillPaint, shadingClip, fillView := resolveShdPaint(ctx, style.fillPaint)
fillClip := intersectClipPath(clipPath, shadingClip) fillClip := intersectClipPath(clipPath, shadingClip)
fp := p fp := p
_, repeat := fillPaint.(*repeatAxialGradient)
if fillClip != nil || repeat {
paths := canvas.Paths(p.Copy().Split())
for _, path := range paths {
path.Close()
}
fp = paths.Merge()
}
fillRule := canvas.NonZero fillRule := canvas.NonZero
if obj.Rule == "Even-Odd" { if obj.Rule == "Even-Odd" {
fillRule = canvas.EvenOdd fillRule = canvas.EvenOdd
@@ -231,8 +239,6 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
} }
} }
if fillClip != nil { if fillClip != nil {
fp = fp.Copy()
fp.Close()
fp = applyClipPath(fp, fillClip) fp = applyClipPath(fp, fillClip)
} }
if style.fillPattern != nil { if style.fillPattern != nil {
@@ -268,7 +274,8 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
ctx.SetDashes(0) ctx.SetDashes(0)
} }
strokeClip := intersectClipPath(clipPath, shadingClip) strokeClip := intersectClipPath(clipPath, shadingClip)
if strokeClip != nil || strokeView != canvas.Identity || style.strokePattern != nil { _, repeat := strokePaint.(*repeatAxialGradient)
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil {
sp = sp.Copy() sp = sp.Copy()
sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance) sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
sp = applyClipPath(sp, strokeClip) sp = applyClipPath(sp, strokeClip)
+65
View File
@@ -15,6 +15,7 @@
package ofdgo package ofdgo
import ( import (
"image/color"
"math" "math"
"strconv" "strconv"
@@ -29,6 +30,28 @@ type shdPaint struct {
period float64 period float64
} }
// repeatAxialGradient 轴向重复渐变
type repeatAxialGradient struct {
*canvas.LinearGradient
period float64
}
// At 获取重复区间内的渐变颜色
// 入参: x X坐标, y Y坐标
// 返回: color.RGBA 渐变颜色
func (g *repeatAxialGradient) At(x, y float64) color.RGBA {
d := g.End.Sub(g.Start)
t := (canvas.Point{X: x, Y: y}).Sub(g.Start).Dot(d) / d.Dot(d) / g.period
return g.Grad.At(t - math.Floor(t))
}
// SetColorSpace 转换重复渐变颜色空间
// 入参: space 颜色空间
// 返回: canvas.Gradient 渐变对象
func (g *repeatAxialGradient) SetColorSpace(space canvas.ColorSpace) canvas.Gradient {
return &repeatAxialGradient{LinearGradient: g.LinearGradient.SetColorSpace(space).(*canvas.LinearGradient), period: g.period}
}
// newShdPaint 创建渐变画刷 // newShdPaint 创建渐变画刷
// 入参: gradient 渐变对象, extend 延伸方向, mapType 绘制方式, mapUnit 渐变区间长度 // 入参: gradient 渐变对象, extend 延伸方向, mapType 绘制方式, mapUnit 渐变区间长度
// 返回: *shdPaint 渐变画刷 // 返回: *shdPaint 渐变画刷
@@ -66,6 +89,9 @@ func resolveShdPaint(ctx *canvas.Context, paint any) (any, *canvas.Path, canvas.
if clip != nil && s.view != canvas.Identity { if clip != nil && s.view != canvas.Identity {
clip = clip.Transform(s.view) clip = clip.Transform(s.view)
} }
if s.mapType == "Repeat" {
return &repeatAxialGradient{LinearGradient: g, period: s.period}, clip, s.view
}
if s.mapType != "Reflect" { if s.mapType != "Reflect" {
return g, clip, s.view return g, clip, s.view
} }
@@ -135,6 +161,10 @@ func transformShdPaint(paint any, parentCTM *Matrix, bx, by, pageH float64, boun
// drawShdPath 绘制渐变路径并保持图形轮廓不变 // drawShdPath 绘制渐变路径并保持图形轮廓不变
// 入参: ctx 画布上下文, path 填充路径, view 渐变变换 // 入参: ctx 画布上下文, path 填充路径, view 渐变变换
func drawShdPath(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix) { func drawShdPath(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix) {
if gradient, ok := ctx.Style.Fill.Gradient.(*repeatAxialGradient); ok {
drawRepeatAxialPath(ctx, path, gradient, view)
return
}
if view == canvas.Identity { if view == canvas.Identity {
ctx.DrawPath(0, 0, path) ctx.DrawPath(0, 0, path)
return return
@@ -144,6 +174,41 @@ func drawShdPath(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix) {
ctx.RenderPath(path.Copy().Transform(view.Inv()), ctx.Style, m) ctx.RenderPath(path.Copy().Transform(view.Inv()), ctx.Style, m)
} }
// drawRepeatAxialPath 按周期裁剪并绘制轴向重复渐变
// 入参: ctx 画布上下文, path 填充路径, gradient 重复渐变, view 渐变变换
func drawRepeatAxialPath(ctx *canvas.Context, path *canvas.Path, gradient *repeatAxialGradient, view canvas.Matrix) {
p := path.Copy()
if ctx.FillRule == canvas.EvenOdd {
p = p.Settle(canvas.EvenOdd)
}
p = p.Transform(view.Inv())
d := gradient.End.Sub(gradient.Start).Mul(gradient.period)
axis := canvas.Matrix{{d.X, -d.Y, gradient.Start.X}, {d.Y, d.X, gradient.Start.Y}}
bounds := p.FastBounds().And(shdCanvasBounds(ctx).Transform(view.Inv()))
area := bounds.Transform(axis.Inv())
origin := ctx.CoordView().Dot(canvas.Point{})
m := ctx.CoordSystemView().Mul(ctx.View()).Translate(origin.X, origin.Y).Mul(view)
style := ctx.Style
style.FillRule = canvas.NonZero
stops := gradient.Grad
if stops[0].Offset > 0 {
stops = append(canvas.Grad{{Offset: 0, Color: stops[0].Color}}, stops...)
}
if last := stops[len(stops)-1]; last.Offset < 1 {
stops = append(append(canvas.Grad(nil), stops...), canvas.Stop{Offset: 1, Color: last.Color})
}
for i, end := math.Floor(area.X0), math.Ceil(area.X1); i < end; i++ {
clip := (canvas.Rect{X0: i, Y0: area.Y0, X1: i + 1, Y1: area.Y1}).ToPath().Transform(axis)
part := applyClipPath(p, clip)
if part.Empty() {
continue
}
start := gradient.Start.Add(d.Mul(i))
style.Fill = canvas.Paint{Gradient: stops.ToLinear(start, start.Add(d))}
ctx.RenderPath(part, style, m)
}
}
// rangeLimits 限制渐变延伸区间 // rangeLimits 限制渐变延伸区间
// 入参: lo 区间起点, hi 区间终点 // 入参: lo 区间起点, hi 区间终点
// 返回: float64 起点, float64 终点 // 返回: float64 起点, float64 终点