-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcontext_clip.go
More file actions
148 lines (122 loc) · 4.5 KB
/
Copy pathcontext_clip.go
File metadata and controls
148 lines (122 loc) · 4.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package gg
import (
"math"
"github.com/gogpu/gg/internal/clip"
)
// Clip sets the current path as the clipping region and clears the path.
// Subsequent drawing operations will be clipped to this region.
// The clip region is intersected with any existing clip regions.
func (c *Context) Clip() {
if c.clipStack == nil {
c.initClipStack()
}
// Path elements are in user-space; clip stack operates in device-space.
// Transform through deviceMatrix to get device coordinates.
devicePath := c.deviceSpacePath()
clipVerbs, clipCoords := convertPathToClipVerbs(devicePath)
// Push the path as a clip region
_ = c.clipStack.PushPath(clipVerbs, clipCoords, true) // anti-aliased by default
// Store the device-space path for GPU depth clipping (GPU-CLIP-003a).
// The GPU DepthClipPipeline fan-tessellates this path at draw time.
c.gpuClipPath = devicePath.Clone()
// Clear the path
c.path.Clear()
}
// ClipPreserve sets the current path as the clipping region but keeps the path.
// This is like Clip() but doesn't clear the path, allowing you to both clip
// and then fill/stroke the same path.
func (c *Context) ClipPreserve() {
if c.clipStack == nil {
c.initClipStack()
}
// Path elements are in user-space; clip stack operates in device-space.
devicePath := c.deviceSpacePath()
clipVerbs, clipCoords := convertPathToClipVerbs(devicePath)
// Push the path as a clip region
_ = c.clipStack.PushPath(clipVerbs, clipCoords, true) // anti-aliased by default
// Store the device-space path for GPU depth clipping (GPU-CLIP-003a).
c.gpuClipPath = devicePath.Clone()
// Path is preserved
}
// ClipRect sets a rectangular clipping region.
// This is a faster alternative to creating a rectangular path and calling Clip().
// The clip region is intersected with any existing clip regions.
func (c *Context) ClipRect(x, y, w, h float64) {
if c.clipStack == nil {
c.initClipStack()
}
// Transform the rectangle corners to device coordinates.
tm := c.totalMatrix()
p1 := tm.TransformPoint(Pt(x, y))
p2 := tm.TransformPoint(Pt(x+w, y+h))
// Create clip rectangle in device coordinates
rect := clip.NewRect(
math.Min(p1.X, p2.X),
math.Min(p1.Y, p2.Y),
math.Abs(p2.X-p1.X),
math.Abs(p2.Y-p1.Y),
)
c.clipStack.PushRect(rect)
}
// ClipRoundRect sets a rounded rectangle clipping region.
// The rectangle is defined by (x, y, w, h) in user coordinates and the
// corners are rounded with the given radius. The radius is clamped to
// min(w, h)/2. If radius is zero, this is equivalent to ClipRect.
//
// On GPU, this uses a two-level clip strategy:
// - Scissor rect (hardware, free) for the bounding box
// - Analytic SDF in the fragment shader for the rounded corners
//
// On CPU, the SDF is evaluated per-pixel during coverage computation.
func (c *Context) ClipRoundRect(x, y, w, h, radius float64) {
if radius <= 0 {
c.ClipRect(x, y, w, h)
return
}
if c.clipStack == nil {
c.initClipStack()
}
// Transform the rectangle corners to device coordinates.
tm := c.totalMatrix()
p1 := tm.TransformPoint(Pt(x, y))
p2 := tm.TransformPoint(Pt(x+w, y+h))
// Create clip rectangle in device coordinates.
devX := math.Min(p1.X, p2.X)
devY := math.Min(p1.Y, p2.Y)
devW := math.Abs(p2.X - p1.X)
devH := math.Abs(p2.Y - p1.Y)
// Scale radius by the total transform scale factor.
scaledRadius := radius * tm.ScaleFactor()
// Clamp to half the smaller dimension.
maxRadius := math.Min(devW, devH) / 2
if scaledRadius > maxRadius {
scaledRadius = maxRadius
}
rect := clip.NewRect(devX, devY, devW, devH)
c.clipStack.PushRRect(rect, scaledRadius)
}
// ResetClip removes all clipping regions, restoring the full canvas as drawable.
func (c *Context) ResetClip() {
if c.clipStack == nil {
return
}
// Reset to physical pixel bounds (clip stack operates in device-space).
bounds := clip.NewRect(0, 0, float64(c.pixmap.Width()), float64(c.pixmap.Height()))
c.clipStack.Reset(bounds)
c.gpuClipPath = nil
}
// initClipStack initializes the clip stack with canvas bounds in device-space.
func (c *Context) initClipStack() {
bounds := clip.NewRect(0, 0, float64(c.pixmap.Width()), float64(c.pixmap.Height()))
c.clipStack = clip.NewClipStack(bounds)
}
// convertPathToClipVerbs converts a gg.Path to clip.PathVerb + coords slices.
// Both PathVerb types have identical byte values, so this is a simple cast.
func convertPathToClipVerbs(p *Path) ([]clip.PathVerb, []float64) {
verbs := p.Verbs()
result := make([]clip.PathVerb, len(verbs))
for i, v := range verbs {
result[i] = clip.PathVerb(v)
}
return result, p.Coords()
}