-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcontext.go
More file actions
1989 lines (1764 loc) · 60.8 KB
/
Copy pathcontext.go
File metadata and controls
1989 lines (1764 loc) · 60.8 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package gg
import (
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"math"
"github.com/gogpu/gg/internal/clip"
"github.com/gogpu/gg/text"
"github.com/gogpu/gpucontext"
)
// Context is the main drawing context.
// It maintains a pixmap, current path, paint state, and transformation stack.
// Context implements io.Closer for proper resource cleanup.
//
// When deviceScale > 1.0 (HiDPI/Retina), the Context maintains a larger physical
// pixmap while exposing logical dimensions to user code. Drawing operations use
// logical coordinates; the Context applies a base scale transform transparently.
type Context struct {
width int // logical width (user-facing)
height int // logical height (user-facing)
pixmap *Pixmap
renderer Renderer
// HiDPI support
deviceScale float64 // physical pixels per logical pixel (default 1.0)
// Current state
path *Path
paint *Paint
face text.Face // Current font face for text drawing
clipStack *clip.ClipStack // Clipping stack
gpuClipPath *Path // device-space clip path for GPU depth clipping (GPU-CLIP-003a)
// Transform and state stack
matrix Matrix // user transform (starts as Identity, user-space only)
deviceMatrix Matrix // device scale transform (Identity when scale=1.0, NEVER modified by user)
stack []Matrix
clipStackDepth []int // Tracks clip stack depth for each Push/Pop
// Layer support
layerStack *layerStack // Layer stack for compositing
basePixmap *Pixmap // Base pixmap when layers are active
// Mask support
mask *Mask // Current alpha mask
maskStack []*Mask // Mask stack for Push/Pop
// Per-frame damage tracking (ADR-021 Level 1).
// List of per-operation bounding boxes — NOT a single union rect.
// Each Fill/Stroke adds its own rect. Passed as-is to PresentWithDamage
// for per-rect OS blit. Merged to bounding box if count exceeds threshold.
frameDamageRects []image.Rectangle
damageTrackingEnabled bool
// Pipeline mode
pipelineMode PipelineMode // GPU pipeline selection mode
// Rasterizer mode
rasterizerMode RasterizerMode // CPU rasterizer selection mode
// Anti-aliasing
antiAlias bool // anti-aliasing enabled (default: true)
antiAliasStack []bool // Push/Pop stack for antiAlias state
// Text rendering
textMode TextMode // text strategy selection (default: Auto)
outlineExtractor *text.OutlineExtractor // lazy: for transform-aware text (Strategy B)
glyphCache *text.GlyphCache // lazy: cached glyph outlines for drawStringAsOutlines
// Per-context GPU render context (isolated pending commands, clips, frame tracking).
// Lazily created when GPURenderContextProvider is available.
// Typed as gpuContextOps (defined in this package) to avoid circular import
// with internal/gpu while maintaining type safety.
gpuCtx gpuContextOps
gpuFallbackWarned bool // true after first global fallback warning (avoid log spam)
// Lifecycle
closed bool // Indicates whether Close has been called
}
// Ensure Context implements io.Closer
var _ io.Closer = (*Context)(nil)
// NewContext creates a new drawing context with the given logical dimensions.
// Optional ContextOption arguments can be used for dependency injection:
//
// // Default software rendering (uses analytic anti-aliasing)
// dc := gg.NewContext(800, 600)
//
// // Custom GPU renderer (dependency injection)
// dc := gg.NewContext(800, 600, gg.WithRenderer(gpuRenderer))
//
// // HiDPI/Retina rendering (logical 800x600, physical 1600x1200)
// dc := gg.NewContext(800, 600, gg.WithDeviceScale(2.0))
//
// When WithDeviceScale is used, the internal pixmap is allocated at physical
// resolution (width*scale x height*scale) while Width/Height return the
// logical dimensions. All drawing operations use logical coordinates.
// NewContextForPixmap creates a Context backed by an existing Pixmap.
// The Context renders directly into the provided pixmap without allocating
// a new one. Used by scene.Renderer for GPU-accelerated scene rendering.
func NewContextForPixmap(pm *Pixmap) *Context {
if pm == nil {
return nil
}
return NewContext(pm.Width(), pm.Height(), func(o *contextOptions) {
o.pixmap = pm
})
}
func NewContext(width, height int, opts ...ContextOption) *Context {
// Apply options
options := defaultOptions()
for _, opt := range opts {
opt(&options)
}
scale := options.deviceScale
if scale <= 0 {
scale = 1.0
}
// Physical dimensions for the pixmap
pw := int(float64(width) * scale)
ph := int(float64(height) * scale)
// Use provided pixmap or create one at physical resolution
pixmap := options.pixmap
if pixmap == nil {
pixmap = NewPixmap(pw, ph)
}
// Use provided renderer or create software renderer at physical resolution
renderer := options.renderer
if renderer == nil {
sr := NewSoftwareRenderer(pw, ph)
if scale > 1.0 {
sr.SetDeviceScale(float32(scale))
}
renderer = sr
}
// Device matrix: maps user coordinates to physical pixels.
// User matrix starts as Identity — user transforms never include device scale.
deviceMatrix := Identity()
if scale != 1.0 {
deviceMatrix = Scale(scale, scale)
}
if scale != 1.0 {
Logger().Info("NewContext HiDPI",
"logical_w", width, "logical_h", height,
"scale", scale,
"physical_w", pw, "physical_h", ph,
)
}
return &Context{
width: width,
height: height,
deviceScale: scale,
pixmap: pixmap,
renderer: renderer,
path: NewPath(),
paint: NewPaint(),
matrix: Identity(),
deviceMatrix: deviceMatrix,
stack: make([]Matrix, 0, 8),
clipStackDepth: make([]int, 0, 8),
pipelineMode: options.pipelineMode,
damageTrackingEnabled: true,
antiAlias: true,
}
}
// NewContextForImage creates a context for drawing on an existing image.
// Optional ContextOption arguments can be used for dependency injection.
// The image dimensions are treated as physical pixel dimensions (deviceScale=1.0).
func NewContextForImage(img image.Image, opts ...ContextOption) *Context {
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
pixmap := FromImage(img)
// Apply options
options := defaultOptions()
for _, opt := range opts {
opt(&options)
}
// Use provided renderer or create software renderer
renderer := options.renderer
if renderer == nil {
renderer = NewSoftwareRenderer(width, height)
}
return &Context{
width: width,
height: height,
deviceScale: 1.0,
pixmap: pixmap,
renderer: renderer,
path: NewPath(),
paint: NewPaint(),
matrix: Identity(),
deviceMatrix: Identity(),
stack: make([]Matrix, 0, 8),
clipStackDepth: make([]int, 0, 8),
pipelineMode: options.pipelineMode,
}
}
// NewContextWithScale creates a new drawing context with the given logical
// dimensions and device scale factor. This is a convenience wrapper for:
//
// gg.NewContext(w, h, gg.WithDeviceScale(scale))
//
// The internal pixmap is allocated at physical resolution (w*scale x h*scale).
// All drawing operations use logical coordinates (w x h).
//
// Example (macOS Retina 2x):
//
// dc := gg.NewContextWithScale(800, 600, 2.0)
// dc.Width() // 800 (logical)
// dc.PixelWidth() // 1600 (physical)
// dc.DrawCircle(400, 300, 100) // logical coordinates
func NewContextWithScale(width, height int, scale float64) *Context {
return NewContext(width, height, WithDeviceScale(scale))
}
// Close releases resources associated with the Context.
// After Close, the Context should not be used.
// Close is idempotent - multiple calls are safe.
// Implements io.Closer.
//
// Close flushes any pending GPU accelerator operations to ensure all
// queued draw commands are rendered before releasing context state.
// Note: Close does NOT shut down the global GPU accelerator itself,
// since it may be shared by other contexts. To release GPU resources
// at application shutdown, call [CloseAccelerator].
func (c *Context) Close() error {
if c.closed {
return nil
}
c.closed = true
// Flush pending GPU operations so queued shapes are not lost.
c.flushGPUAccelerator()
// Close per-context GPU render context if it was created.
if c.gpuCtx != nil {
type gpuCtxCloser interface {
Close()
}
if closer, ok := c.gpuCtx.(gpuCtxCloser); ok {
closer.Close()
}
c.gpuCtx = nil
}
// Clear path to release memory
c.ClearPath()
// Clear state stack
c.stack = nil
c.clipStackDepth = nil
c.maskStack = nil
c.mask = nil
c.gpuClipPath = nil
return nil
}
// SetPipelineMode sets the GPU rendering pipeline mode.
// See PipelineMode for available modes.
//
// If the registered accelerator implements PipelineModeAware, the mode is
// propagated so the accelerator can route operations to the correct pipeline
// (render pass vs compute).
func (c *Context) SetPipelineMode(mode PipelineMode) {
c.pipelineMode = mode
if rc := c.gpuCtxOps(); rc != nil {
rc.SetPipelineMode(mode)
} else if a := Accelerator(); a != nil {
if pma, ok := a.(PipelineModeAware); ok {
pma.SetPipelineMode(mode)
}
}
}
// PipelineMode returns the current pipeline mode.
func (c *Context) PipelineMode() PipelineMode {
return c.pipelineMode
}
// SetRasterizerMode sets the rasterization strategy for this context.
// RasterizerAuto (default) uses intelligent auto-selection based on path
// complexity, bounding box area, and shape type.
// Other modes force a specific algorithm, bypassing auto-selection.
//
// The mode is per-Context — different contexts can use different strategies.
func (c *Context) SetRasterizerMode(mode RasterizerMode) {
c.rasterizerMode = mode
}
// RasterizerMode returns the current rasterizer mode.
func (c *Context) RasterizerMode() RasterizerMode {
return c.rasterizerMode
}
// SetAntiAlias enables or disables anti-aliasing for geometry rendering.
//
// When enabled (default), shapes are rendered with smooth edges using analytic
// anti-aliasing (Skia AAA). When disabled, shapes are rendered with binary
// coverage (fully inside or fully outside) producing crisp, aliased edges.
//
// This is useful for pixel art, retro-style graphics, technical drawings,
// and any use case where sub-pixel blending is undesirable.
//
// Text anti-aliasing is controlled independently via SetTextMode.
// The anti-aliasing state participates in Push/Pop.
//
// Reference: Skia SkPaint::setAntiAlias, Cairo cairo_set_antialias,
// tiny-skia Paint.anti_alias.
func (c *Context) SetAntiAlias(enabled bool) {
c.antiAlias = enabled
}
// AntiAlias returns whether anti-aliasing is enabled for geometry rendering.
func (c *Context) AntiAlias() bool {
return c.antiAlias
}
// SetTextMode sets the text rendering strategy.
// See TextMode constants for available strategies.
//
// The mode is per-Context — different contexts can use different strategies.
func (c *Context) SetTextMode(mode TextMode) {
c.textMode = mode
}
// TextMode returns the current text rendering strategy.
func (c *Context) TextMode() TextMode {
return c.textMode
}
// SetLCDLayout sets the LCD subpixel layout for ClearType text rendering.
// Use LCDLayoutRGB for most monitors, LCDLayoutBGR for rare BGR panels,
// or LCDLayoutNone to disable subpixel rendering (grayscale, the default).
//
// When a GPU accelerator is registered and implements LCDLayoutAware,
// the layout is propagated so the glyph mask engine rasterizes glyphs
// with 3x horizontal oversampling and the GPU uses the LCD fragment shader.
//
// The setting is per-Context. Call this before drawing text.
func (c *Context) SetLCDLayout(layout LCDLayout) {
a := Accelerator()
if a == nil {
return
}
if la, ok := a.(LCDLayoutAware); ok {
la.SetLCDLayout(layout)
}
}
// Width returns the logical width of the context.
// This is the coordinate space used by drawing operations.
// For the physical pixel dimensions, use PixelWidth.
func (c *Context) Width() int {
return c.width
}
// Height returns the logical height of the context.
// This is the coordinate space used by drawing operations.
// For the physical pixel dimensions, use PixelHeight.
func (c *Context) Height() int {
return c.height
}
// PixelWidth returns the physical pixel width of the internal pixmap.
// This equals Width() * DeviceScale(), rounded to int.
// On non-HiDPI displays (scale=1.0), this equals Width().
func (c *Context) PixelWidth() int {
return int(float64(c.width) * c.deviceScale)
}
// PixelHeight returns the physical pixel height of the internal pixmap.
// This equals Height() * DeviceScale(), rounded to int.
// On non-HiDPI displays (scale=1.0), this equals Height().
func (c *Context) PixelHeight() int {
return int(float64(c.height) * c.deviceScale)
}
// DeviceScale returns the device scale factor (physical pixels per logical pixel).
// Default is 1.0. On Retina/HiDPI displays, typical values are 2.0 or 3.0.
func (c *Context) DeviceScale() float64 {
return c.deviceScale
}
// SetDeviceScale changes the device scale factor on an existing context.
// This reallocates the internal pixmap at the new physical resolution
// and adjusts the base transform. The logical dimensions (Width, Height)
// remain unchanged.
//
// Use this when the window moves to a display with a different scale factor.
// Scale must be > 0; values <= 0 are ignored.
func (c *Context) SetDeviceScale(scale float64) {
if scale <= 0 || scale == c.deviceScale {
return
}
oldScale := c.deviceScale
c.deviceScale = scale
// Physical dimensions
pw := int(float64(c.width) * scale)
ph := int(float64(c.height) * scale)
Logger().Info("SetDeviceScale",
"old_scale", oldScale, "new_scale", scale,
"logical_w", c.width, "logical_h", c.height,
"physical_w", pw, "physical_h", ph,
)
// Reallocate pixmap at new physical resolution
c.pixmap = NewPixmap(pw, ph)
// Update renderer dimensions and device scale
if sr, ok := c.renderer.(*SoftwareRenderer); ok {
sr.Resize(pw, ph)
sr.SetDeviceScale(float32(scale))
}
// Update device matrix. User matrix (c.matrix) is NOT touched —
// it contains only user transforms and is independent of device scale.
c.deviceMatrix = Identity()
if scale != 1.0 {
c.deviceMatrix = Scale(scale, scale)
}
// Reset clip stack (clip regions are in pixel coordinates)
c.clipStack = nil
c.gpuClipPath = nil
c.ClearPath()
}
// Image returns the context's image.
func (c *Context) Image() image.Image {
return c.pixmap.ToImage()
}
// SavePNG saves the context to a PNG file.
func (c *Context) SavePNG(path string) error {
_ = c.FlushGPU() // Flush pending GPU shapes before reading pixels.
return c.pixmap.SavePNG(path)
}
// Clear resets the entire context to transparent (zero alpha).
// To fill with a specific background color, use [ClearWithColor].
func (c *Context) Clear() {
c.pixmap.Clear(Transparent)
}
// ClearWithColor fills the entire context with the specified color.
// This is the recommended way to set a background color before drawing.
func (c *Context) ClearWithColor(col RGBA) {
c.pixmap.Clear(col)
}
// maxDamageRects is the threshold above which individual rects are merged
// into a single bounding box. Too many small rects = too many OS blit calls.
// Wayland/Android compositors use similar thresholds.
const maxDamageRects = 16
// FrameDamage returns the list of damage rectangles from draw operations
// this frame. Each rect corresponds to one or more Fill/Stroke operations.
// Used by ggcanvas → SetDamageRects → PresentWithDamage for per-rect OS blit.
// Returns nil if no drawing operations occurred.
func (c *Context) FrameDamage() []image.Rectangle {
if len(c.frameDamageRects) == 0 {
return nil
}
return c.frameDamageRects
}
// FrameDamageUnion returns the bounding box of all damage rects this frame.
// Convenience method for debug display or single-rect consumers.
func (c *Context) FrameDamageUnion() image.Rectangle {
var r image.Rectangle
for _, dr := range c.frameDamageRects {
r = r.Union(dr)
}
return r
}
// ResetFrameDamage clears the per-frame damage accumulator.
// Call at the start of each frame before drawing operations.
func (c *Context) ResetFrameDamage() {
c.frameDamageRects = c.frameDamageRects[:0]
}
// SetDamageTracking enables or disables per-operation damage recording.
// When disabled, Fill/Stroke do not append to FrameDamage.
// Used by retained-mode compositors to suppress damage during replay
// of cached (clean) scene content (ADR-021 false positive fix).
func (c *Context) SetDamageTracking(enabled bool) {
c.damageTrackingEnabled = enabled
}
// TrackDamageRect registers an external damage rectangle on the surface.
// Use this for compositor operations that modify the surface but don't use
// Fill/Stroke (e.g., DrawGPUTexture for dirty RepaintBoundary overlays).
// No-op when damage tracking is disabled or rect is empty.
//
// Callers with retained-mode knowledge (e.g., ui widget tree) should call
// this for each dirty boundary after compositing, so that FrameDamage()
// accurately reflects which surface regions changed this frame.
//
// Bounds are in logical (user-space) coordinates. The context automatically
// scales them to physical pixels via deviceScale for the OS compositor.
func (c *Context) TrackDamageRect(bounds image.Rectangle) {
c.trackDamage(bounds)
}
// trackDamage adds a damage rectangle for the current draw operation.
// No-op when damage tracking is disabled (cached scene replay).
// If rect count exceeds maxDamageRects, merges all into bounding box.
func (c *Context) trackDamage(bounds image.Rectangle) {
if !c.damageTrackingEnabled || bounds.Empty() {
return
}
// Scale logical damage rect to physical pixels for OS compositor APIs
// (Vulkan VK_KHR_incremental_present, DX12 Present1, EGL, Wayland damage_buffer).
// Floor/Ceil ensures conservative rounding with no pixel gaps.
if !c.deviceMatrix.IsIdentity() {
s := c.deviceScale
bounds = image.Rect(
int(math.Floor(float64(bounds.Min.X)*s)),
int(math.Floor(float64(bounds.Min.Y)*s)),
int(math.Ceil(float64(bounds.Max.X)*s)),
int(math.Ceil(float64(bounds.Max.Y)*s)),
)
}
c.frameDamageRects = append(c.frameDamageRects, bounds)
if len(c.frameDamageRects) > maxDamageRects {
merged := c.frameDamageRects[0]
for _, r := range c.frameDamageRects[1:] {
merged = merged.Union(r)
}
c.frameDamageRects = c.frameDamageRects[:1]
c.frameDamageRects[0] = merged
}
}
// FillRectCPU fills a rectangle directly on the CPU pixmap without engaging
// the GPU SDF accelerator. Coordinates are in user space (device scale applied
// automatically). Pending GPU shapes are flushed first for correct z-ordering.
//
// Use for operations where GPU acceleration is counterproductive, such as
// dirty-region background clearing in retained-mode compositors. Without this,
// DrawRectangle+Fill routes through SDF accelerator → blocks non-MSAA blit path.
//
// See ADR-016, TASK-GG-COMPOSITOR-003.
func (c *Context) FillRectCPU(x, y, w, h float64, col RGBA) {
c.flushGPUAccelerator()
ctm := c.totalMatrix()
tl := ctm.TransformPoint(Pt(x, y))
br := ctm.TransformPoint(Pt(x+w, y+h))
px0 := int(tl.X)
py0 := int(tl.Y)
px1 := int(br.X + 0.5)
py1 := int(br.Y + 0.5)
pr := uint8(clamp255(col.R * col.A * 255))
pg := uint8(clamp255(col.G * col.A * 255))
pb := uint8(clamp255(col.B * col.A * 255))
pa := uint8(clamp255(col.A * 255))
c.pixmap.FillRect(image.Rect(px0, py0, px1, py1), pr, pg, pb, pa)
}
// SetColor sets the current drawing color.
func (c *Context) SetColor(col color.Color) {
c.paint.solidColor = FromColor(col)
c.paint.isSolid = true
c.paint.Brush = nil
c.paint.Pattern = nil
}
// SetRGB sets the current color using RGB values (0-1).
func (c *Context) SetRGB(r, g, b float64) {
c.paint.solidColor = RGBA{R: r, G: g, B: b, A: 1}
c.paint.isSolid = true
c.paint.Brush = nil
c.paint.Pattern = nil
}
// SetRGBA sets the current color using RGBA values (0-1).
func (c *Context) SetRGBA(r, g, b, a float64) {
c.paint.solidColor = RGBA{R: r, G: g, B: b, A: a}
c.paint.isSolid = true
c.paint.Brush = nil
c.paint.Pattern = nil
}
// SetHexColor sets the current color using a hex string.
func (c *Context) SetHexColor(hex string) {
c.paint.solidColor = Hex(hex)
c.paint.isSolid = true
c.paint.Brush = nil
c.paint.Pattern = nil
}
// SetFillBrush sets the brush used for fill operations.
// This is the preferred way to set fill styling in new code.
//
// Example:
//
// ctx.SetFillBrush(gg.Solid(gg.Red))
// ctx.SetFillBrush(gg.SolidHex("#FF5733"))
// ctx.SetFillBrush(gg.HorizontalGradient(gg.Red, gg.Blue, 0, 100))
func (c *Context) SetFillBrush(b Brush) {
c.paint.SetBrush(b)
}
// SetStrokeBrush sets the brush used for stroke operations.
// Note: In the current implementation, fill and stroke share the same brush.
// This method is provided for API symmetry and future extensibility.
//
// Example:
//
// ctx.SetStrokeBrush(gg.Solid(gg.Black))
// ctx.SetStrokeBrush(gg.SolidRGB(0.5, 0.5, 0.5))
func (c *Context) SetStrokeBrush(b Brush) {
c.paint.SetBrush(b)
}
// FillBrush returns the current fill brush.
func (c *Context) FillBrush() Brush {
return c.paint.GetBrush()
}
// StrokeBrush returns the current stroke brush.
// Note: In the current implementation, fill and stroke share the same brush.
func (c *Context) StrokeBrush() Brush {
return c.paint.GetBrush()
}
// SetLineWidth sets the line width for stroking.
func (c *Context) SetLineWidth(width float64) {
c.paint.LineWidth = width
}
// SetLineCap sets the line cap style.
func (c *Context) SetLineCap(lineCap LineCap) {
c.paint.LineCap = lineCap
}
// SetLineJoin sets the line join style.
func (c *Context) SetLineJoin(join LineJoin) {
c.paint.LineJoin = join
}
// SetFillRule sets the fill rule.
func (c *Context) SetFillRule(rule FillRule) {
c.paint.FillRule = rule
}
// SetMiterLimit sets the miter limit for line joins.
func (c *Context) SetMiterLimit(limit float64) {
c.paint.MiterLimit = limit
}
// SetStroke sets the complete stroke style.
// This is the preferred way to configure stroke properties.
//
// Example:
//
// ctx.SetStroke(gg.DefaultStroke().WithWidth(2).WithCap(gg.LineCapRound))
// ctx.SetStroke(gg.DashedStroke(5, 3))
func (c *Context) SetStroke(stroke Stroke) {
c.paint.SetStroke(stroke)
}
// GetStroke returns the current stroke style.
func (c *Context) GetStroke() Stroke {
return c.paint.GetStroke()
}
// SetDash sets the dash pattern for stroking.
// Pass alternating dash and gap lengths.
// Passing no arguments clears the dash pattern (returns to solid lines).
//
// Example:
//
// ctx.SetDash(5, 3) // 5 units dash, 3 units gap
// ctx.SetDash(10, 5, 2, 5) // complex pattern
// ctx.SetDash() // clear dash (solid line)
func (c *Context) SetDash(lengths ...float64) {
if len(lengths) == 0 {
c.ClearDash()
return
}
dash := NewDash(lengths...)
if dash == nil {
c.ClearDash()
return
}
// Ensure we have a Stroke to set the dash on
if c.paint.Stroke == nil {
stroke := c.paint.GetStroke()
c.paint.Stroke = &stroke
}
c.paint.Stroke.Dash = dash
}
// SetDashOffset sets the starting offset into the dash pattern.
// This has no effect if no dash pattern is set.
func (c *Context) SetDashOffset(offset float64) {
if c.paint.Stroke == nil {
// Create stroke from legacy fields if needed
stroke := c.paint.GetStroke()
c.paint.Stroke = &stroke
}
if c.paint.Stroke.Dash != nil {
c.paint.Stroke.Dash = c.paint.Stroke.Dash.WithOffset(offset)
}
}
// ClearDash removes the dash pattern, returning to solid lines.
func (c *Context) ClearDash() {
if c.paint.Stroke != nil {
c.paint.Stroke.Dash = nil
}
}
// IsDashed returns true if the current stroke uses a dash pattern.
func (c *Context) IsDashed() bool {
return c.paint.IsDashed()
}
// MoveTo starts a new subpath at the given point.
func (c *Context) MoveTo(x, y float64) {
p := c.matrix.TransformPoint(Pt(x, y))
c.path.MoveTo(p.X, p.Y)
}
// LineTo adds a line to the current path.
func (c *Context) LineTo(x, y float64) {
p := c.matrix.TransformPoint(Pt(x, y))
c.path.LineTo(p.X, p.Y)
}
// QuadraticTo adds a quadratic Bezier curve to the current path.
func (c *Context) QuadraticTo(cx, cy, x, y float64) {
cp := c.matrix.TransformPoint(Pt(cx, cy))
p := c.matrix.TransformPoint(Pt(x, y))
c.path.QuadraticTo(cp.X, cp.Y, p.X, p.Y)
}
// CubicTo adds a cubic Bezier curve to the current path.
func (c *Context) CubicTo(c1x, c1y, c2x, c2y, x, y float64) {
cp1 := c.matrix.TransformPoint(Pt(c1x, c1y))
cp2 := c.matrix.TransformPoint(Pt(c2x, c2y))
p := c.matrix.TransformPoint(Pt(x, y))
c.path.CubicTo(cp1.X, cp1.Y, cp2.X, cp2.Y, p.X, p.Y)
}
// ClosePath closes the current subpath.
func (c *Context) ClosePath() {
c.path.Close()
}
// ClearPath clears the current path.
func (c *Context) ClearPath() {
c.path.Clear()
}
// SetPath replaces the current path with p.
// The path is copied — subsequent modifications to p do not affect the context.
// Use this to render pre-built paths (e.g., from ParseSVGPath):
//
// path, _ := gg.ParseSVGPath("M10,10 L90,10 L90,90 Z")
// dc.SetPath(path)
// dc.Fill()
func (c *Context) SetPath(p *Path) {
c.path.Clear()
if p != nil {
c.path.Append(p)
}
}
// AppendPath appends the elements of p to the current path without clearing it.
// This allows combining multiple sub-paths before a single Fill or Stroke call.
// Note: path coordinates are copied as-is (not transformed by the current matrix).
// Use DrawPath for transform-aware path rendering.
func (c *Context) AppendPath(p *Path) {
if p != nil {
c.path.Append(p)
}
}
// DrawPath replays the elements of p through the current transform matrix,
// replacing the current path. Unlike SetPath (which copies raw coordinates),
// DrawPath applies the current matrix (Translate, Scale, Rotate) to all points.
// After DrawPath, call Fill() or Stroke() to render.
//
// This is the correct way to render pre-built paths (e.g., from ParseSVGPath)
// with transforms:
//
// path, _ := gg.ParseSVGPath("M10,10 L90,10 L90,90 Z")
// dc.Push()
// dc.Translate(x, y)
// dc.Scale(0.5, 0.5)
// dc.DrawPath(path)
// dc.Fill()
// dc.Pop()
func (c *Context) DrawPath(p *Path) {
c.ClearPath()
if p == nil {
return
}
p.Iterate(func(verb PathVerb, coords []float64) {
switch verb {
case MoveTo:
c.MoveTo(coords[0], coords[1])
case LineTo:
c.LineTo(coords[0], coords[1])
case QuadTo:
c.QuadraticTo(coords[0], coords[1], coords[2], coords[3])
case CubicTo:
c.CubicTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5])
case Close:
c.ClosePath()
}
})
}
// FillPath is a convenience method that replays path p through the current
// transform, fills it, and clears the path. Equivalent to DrawPath(p) + Fill().
func (c *Context) FillPath(p *Path) error {
c.DrawPath(p)
return c.Fill()
}
// StrokePath is a convenience method that replays path p through the current
// transform, strokes it, and clears the path. Equivalent to DrawPath(p) + Stroke().
func (c *Context) StrokePath(p *Path) error {
c.DrawPath(p)
return c.Stroke()
}
// NewSubPath starts a new subpath without closing the previous one.
func (c *Context) NewSubPath() {
// In most implementations, just starting with MoveTo creates a new subpath
// This is a no-op but provided for API compatibility
}
// Fill fills the current path and clears it.
// If a GPU accelerator is registered and supports the path, it is used first.
// Otherwise, the software renderer handles the operation.
// The RasterizerMode set via SetRasterizerMode controls algorithm selection.
// Returns an error if the rendering operation fails.
func (c *Context) Fill() error {
c.trackDamage(c.path.Bounds())
err := c.doFill()
c.path.Clear()
return err
}
// Stroke strokes the current path and clears it.
// If a GPU accelerator is registered and supports the path, it is used first.
// Otherwise, the software renderer handles the operation.
// The RasterizerMode set via SetRasterizerMode controls algorithm selection.
// Returns an error if the rendering operation fails.
func (c *Context) Stroke() error {
c.trackDamage(c.path.Bounds())
err := c.doStroke()
c.path.Clear()
return err
}
// FillPreserve fills the current path without clearing it.
// If a GPU accelerator is registered and supports the path, it is used first.
// Otherwise, the software renderer handles the operation.
// Returns an error if the rendering operation fails.
func (c *Context) FillPreserve() error {
return c.doFill()
}
// StrokePreserve strokes the current path without clearing it.
// If a GPU accelerator is registered and supports the path, it is used first.
// Otherwise, the software renderer handles the operation.
// Returns an error if the rendering operation fails.
func (c *Context) StrokePreserve() error {
return c.doStroke()
}
// Push saves the current state (transform, paint, clip, and mask).
func (c *Context) Push() {
c.stack = append(c.stack, c.matrix)
// Save current clip stack depth
depth := 0
if c.clipStack != nil {
depth = c.clipStack.Depth()
}
c.clipStackDepth = append(c.clipStackDepth, depth)
// Save current mask (clone if exists)
var maskCopy *Mask
if c.mask != nil {
maskCopy = c.mask.Clone()
}
c.maskStack = append(c.maskStack, maskCopy)
// Save current anti-aliasing state
c.antiAliasStack = append(c.antiAliasStack, c.antiAlias)
}
// Pop restores the last saved state.
func (c *Context) Pop() {
if len(c.stack) == 0 {
return
}
// Restore transform matrix
c.matrix = c.stack[len(c.stack)-1]
c.stack = c.stack[:len(c.stack)-1]
// Restore clip stack depth
if len(c.clipStackDepth) > 0 {
targetDepth := c.clipStackDepth[len(c.clipStackDepth)-1]
c.clipStackDepth = c.clipStackDepth[:len(c.clipStackDepth)-1]
// Pop clip stack entries until we reach the target depth
if c.clipStack != nil {
for c.clipStack.Depth() > targetDepth {
c.clipStack.Pop()
}
// Clear GPU clip path if all path clips were popped.
if c.gpuClipPath != nil && c.clipStack.IsRRectOnly() {
c.gpuClipPath = nil
}
}
}
// Restore mask
if len(c.maskStack) > 0 {
c.mask = c.maskStack[len(c.maskStack)-1]
c.maskStack = c.maskStack[:len(c.maskStack)-1]
}
// Restore anti-aliasing state
if len(c.antiAliasStack) > 0 {
c.antiAlias = c.antiAliasStack[len(c.antiAliasStack)-1]
c.antiAliasStack = c.antiAliasStack[:len(c.antiAliasStack)-1]
}
}
// Identity resets the user transformation matrix to the identity matrix.
// Device scale is applied separately at rendering boundaries (not in the CTM),
// so Identity() always resets to a pure identity matrix regardless of scale.
func (c *Context) Identity() {
c.matrix = Identity()
}
// Translate applies a translation to the transformation matrix.
func (c *Context) Translate(x, y float64) {
c.matrix = c.matrix.Multiply(Translate(x, y))
}
// Scale applies a scaling transformation.
func (c *Context) Scale(x, y float64) {
c.matrix = c.matrix.Multiply(Scale(x, y))
}
// Rotate applies a rotation (angle in radians).
func (c *Context) Rotate(angle float64) {
c.matrix = c.matrix.Multiply(Rotate(angle))
}
// RotateAbout rotates around a specific point.
func (c *Context) RotateAbout(angle, x, y float64) {
c.Translate(x, y)
c.Rotate(angle)
c.Translate(-x, -y)
}