368 lines
9.3 KiB
Go
368 lines
9.3 KiB
Go
package world
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
|
|
"github.com/fogleman/gg"
|
|
)
|
|
|
|
// PrimitiveDrawer is a low-level drawing backend used by the world renderer.
|
|
//
|
|
// The renderer is responsible for all torus logic, viewport/margin logic,
|
|
// coordinate projection, and primitive duplication. This interface only accepts
|
|
// final canvas pixel coordinates and exposes the minimum drawing operations
|
|
// needed to build and render paths.
|
|
//
|
|
// AddPoint, AddLine, and AddCircle append geometry to the current path.
|
|
// They do not render by themselves. The caller must finalize the path by
|
|
// calling Stroke or Fill.
|
|
//
|
|
// Save and Restore are intended for temporary local state changes such as
|
|
// clipping, colors, line width, or dash settings. After Restore, the outer
|
|
// drawing state must be visible again.
|
|
type PrimitiveDrawer interface {
|
|
// Save stores the current drawing state.
|
|
Save()
|
|
|
|
// Restore restores the most recently saved drawing state.
|
|
Restore()
|
|
|
|
// ResetClip clears the current clipping region completely.
|
|
ResetClip()
|
|
|
|
// ClipRect intersects the current clipping region with the given rectangle
|
|
// in canvas pixel coordinates.
|
|
ClipRect(x, y, w, h float64)
|
|
|
|
// SetStrokeColor sets the color used by Stroke.
|
|
SetStrokeColor(c color.Color)
|
|
|
|
// SetFillColor sets the color used by Fill.
|
|
SetFillColor(c color.Color)
|
|
|
|
// SetLineWidth sets the line width used by Stroke.
|
|
SetLineWidth(width float64)
|
|
|
|
// SetDash sets the dash pattern used by Stroke.
|
|
// Passing no values clears the current dash pattern.
|
|
SetDash(dashes ...float64)
|
|
|
|
// SetDashOffset sets the dash phase used by Stroke.
|
|
SetDashOffset(offset float64)
|
|
|
|
// AddPoint appends a point marker centered at (x, y) with radius r
|
|
// to the current path in canvas pixel coordinates.
|
|
AddPoint(x, y, r float64)
|
|
|
|
// AddLine appends a line segment to the current path in canvas pixel coordinates.
|
|
AddLine(x1, y1, x2, y2 float64)
|
|
|
|
// AddCircle appends a circle to the current path in canvas pixel coordinates.
|
|
AddCircle(cx, cy, r float64)
|
|
|
|
// Stroke renders the current path using the current stroke state.
|
|
Stroke()
|
|
|
|
// Fill renders the current path using the current fill state.
|
|
Fill()
|
|
|
|
// CopyShift shifts backing pixels by (dx,dy). Newly exposed areas become transparent/undefined;
|
|
// caller is expected to ClearRectTo() the dirty areas before drawing.
|
|
CopyShift(dx, dy int)
|
|
|
|
// Clear operations must NOT change clip state.
|
|
ClearAllTo(bg color.Color)
|
|
ClearRectTo(x, y, w, h int, bg color.Color)
|
|
|
|
DrawImage(img image.Image, x, y int)
|
|
|
|
DrawImageScaled(img image.Image, x, y, w, h int)
|
|
}
|
|
|
|
// ggClipRect stores one clip rectangle in canvas pixel coordinates.
|
|
// GGDrawer replays these rectangles on Restore because gg.Context Push/Pop
|
|
// do not restore clip masks the way this package expects.
|
|
type ggClipRect struct {
|
|
x, y float64
|
|
w, h float64
|
|
}
|
|
|
|
// GGDrawer is a PrimitiveDrawer implementation backed by gg.Context.
|
|
//
|
|
// It intentionally does not perform any world logic. It only forwards already
|
|
// projected canvas coordinates to gg while additionally maintaining a clip stack
|
|
// compatible with this package's Save/Restore contract.
|
|
type GGDrawer struct {
|
|
DC *gg.Context
|
|
|
|
clips []ggClipRect
|
|
clipStack [][]ggClipRect
|
|
|
|
// scratch is a reusable buffer for CopyShift to avoid allocations.
|
|
scratch *image.RGBA
|
|
}
|
|
|
|
// Save stores the current gg state and the current logical clip stack.
|
|
func (d *GGDrawer) Save() {
|
|
d.DC.Push()
|
|
|
|
snapshot := append([]ggClipRect(nil), d.clips...)
|
|
d.clipStack = append(d.clipStack, snapshot)
|
|
}
|
|
|
|
// Restore restores the previous gg state and rebuilds the outer clip state.
|
|
//
|
|
// gg.Context.Pop restores most state from the stack, but its clip mask handling
|
|
// does not match this package's expected Save/Restore semantics. To preserve the
|
|
// contract, GGDrawer explicitly resets the clip and replays the previously saved
|
|
// clip rectangles after Pop.
|
|
func (d *GGDrawer) Restore() {
|
|
if len(d.clipStack) == 0 {
|
|
panic("GGDrawer: Restore without matching Save")
|
|
}
|
|
|
|
snapshot := d.clipStack[len(d.clipStack)-1]
|
|
d.clipStack = d.clipStack[:len(d.clipStack)-1]
|
|
|
|
d.DC.Pop()
|
|
|
|
d.clips = append([]ggClipRect(nil), snapshot...)
|
|
d.DC.ResetClip()
|
|
for _, clip := range d.clips {
|
|
d.DC.DrawRectangle(clip.x, clip.y, clip.w, clip.h)
|
|
d.DC.Clip()
|
|
}
|
|
}
|
|
|
|
// ResetClip clears the current clipping region and the logical clip stack
|
|
// for the active state frame.
|
|
func (d *GGDrawer) ResetClip() {
|
|
d.DC.ResetClip()
|
|
d.clips = nil
|
|
}
|
|
|
|
// ClipRect intersects the current clipping region with the given rectangle
|
|
// and records it so the clip can be reconstructed after Restore.
|
|
func (d *GGDrawer) ClipRect(x, y, w, h float64) {
|
|
d.DC.DrawRectangle(x, y, w, h)
|
|
d.DC.Clip()
|
|
|
|
d.clips = append(d.clips, ggClipRect{x: x, y: y, w: w, h: h})
|
|
}
|
|
|
|
// SetStrokeColor sets the stroke color by installing a solid stroke pattern.
|
|
func (d *GGDrawer) SetStrokeColor(c color.Color) {
|
|
d.DC.SetStrokeStyle(gg.NewSolidPattern(c))
|
|
}
|
|
|
|
// SetFillColor sets the fill color by installing a solid fill pattern.
|
|
func (d *GGDrawer) SetFillColor(c color.Color) {
|
|
d.DC.SetFillStyle(gg.NewSolidPattern(c))
|
|
}
|
|
|
|
// SetLineWidth sets the line width used for stroking.
|
|
func (d *GGDrawer) SetLineWidth(width float64) {
|
|
d.DC.SetLineWidth(width)
|
|
}
|
|
|
|
// SetDash sets the dash pattern used for stroking.
|
|
func (d *GGDrawer) SetDash(dashes ...float64) {
|
|
d.DC.SetDash(dashes...)
|
|
}
|
|
|
|
// SetDashOffset sets the dash phase used for stroking.
|
|
func (d *GGDrawer) SetDashOffset(offset float64) {
|
|
d.DC.SetDashOffset(offset)
|
|
}
|
|
|
|
// AddPoint appends a point marker to the current path.
|
|
func (d *GGDrawer) AddPoint(x, y, r float64) {
|
|
d.DC.DrawPoint(x, y, r)
|
|
}
|
|
|
|
// AddLine appends a line segment to the current path.
|
|
func (d *GGDrawer) AddLine(x1, y1, x2, y2 float64) {
|
|
d.DC.DrawLine(x1, y1, x2, y2)
|
|
}
|
|
|
|
// AddCircle appends a circle to the current path.
|
|
func (d *GGDrawer) AddCircle(cx, cy, r float64) {
|
|
d.DC.DrawCircle(cx, cy, r)
|
|
}
|
|
|
|
// Stroke renders the current path using the current stroke state.
|
|
func (d *GGDrawer) Stroke() {
|
|
d.DC.Stroke()
|
|
}
|
|
|
|
// Fill renders the current path using the current fill state.
|
|
func (d *GGDrawer) Fill() {
|
|
d.DC.Fill()
|
|
}
|
|
|
|
// CopyShift shifts the backing RGBA image by (dx, dy) pixels.
|
|
// It clears newly exposed areas to transparent.
|
|
func (d *GGDrawer) CopyShift(dx, dy int) {
|
|
if dx == 0 && dy == 0 {
|
|
return
|
|
}
|
|
|
|
img, ok := d.DC.Image().(*image.RGBA)
|
|
if !ok || img == nil {
|
|
panic("GGDrawer.CopyShift: backing image is not *image.RGBA")
|
|
}
|
|
|
|
b := img.Bounds()
|
|
w := b.Dx()
|
|
h := b.Dy()
|
|
if w <= 0 || h <= 0 {
|
|
return
|
|
}
|
|
|
|
adx := abs(dx)
|
|
ady := abs(dy)
|
|
if adx >= w || ady >= h {
|
|
// Everything shifts out of bounds => just clear.
|
|
for i := range img.Pix {
|
|
img.Pix[i] = 0
|
|
}
|
|
return
|
|
}
|
|
|
|
// Prepare scratch with the same bounds.
|
|
if d.scratch == nil || d.scratch.Bounds().Dx() != w || d.scratch.Bounds().Dy() != h {
|
|
d.scratch = image.NewRGBA(b)
|
|
} else {
|
|
// Clear scratch to transparent.
|
|
for i := range d.scratch.Pix {
|
|
d.scratch.Pix[i] = 0
|
|
}
|
|
}
|
|
|
|
// Compute source/destination rectangles.
|
|
dstX0 := 0
|
|
dstY0 := 0
|
|
srcX0 := 0
|
|
srcY0 := 0
|
|
if dx > 0 {
|
|
dstX0 = dx
|
|
} else {
|
|
srcX0 = -dx
|
|
}
|
|
if dy > 0 {
|
|
dstY0 = dy
|
|
} else {
|
|
srcY0 = -dy
|
|
}
|
|
|
|
copyW := w - max(dstX0, srcX0)
|
|
copyH := h - max(dstY0, srcY0)
|
|
if copyW <= 0 || copyH <= 0 {
|
|
for i := range img.Pix {
|
|
img.Pix[i] = 0
|
|
}
|
|
return
|
|
}
|
|
|
|
// Copy row-by-row (RGBA, 4 bytes per pixel).
|
|
for row := 0; row < copyH; row++ {
|
|
srcY := srcY0 + row
|
|
dstY := dstY0 + row
|
|
|
|
srcOff := srcY*img.Stride + srcX0*4
|
|
dstOff := dstY*d.scratch.Stride + dstX0*4
|
|
n := copyW * 4
|
|
|
|
copy(d.scratch.Pix[dstOff:dstOff+n], img.Pix[srcOff:srcOff+n])
|
|
}
|
|
|
|
// Swap buffers by copying scratch into img.
|
|
// (We keep img pointer stable for gg.Context.)
|
|
copy(img.Pix, d.scratch.Pix)
|
|
}
|
|
|
|
func (d *GGDrawer) ClearAllTo(bg color.Color) {
|
|
img, ok := d.DC.Image().(*image.RGBA)
|
|
if !ok || img == nil {
|
|
panic("GGDrawer.ClearAllTo: backing image is not *image.RGBA")
|
|
}
|
|
|
|
r, g, b, a := bg.RGBA()
|
|
// Convert from 16-bit range to 8-bit.
|
|
R := byte(r >> 8)
|
|
G := byte(g >> 8)
|
|
B := byte(b >> 8)
|
|
A := byte(a >> 8)
|
|
|
|
p := img.Pix
|
|
for i := 0; i+3 < len(p); i += 4 {
|
|
p[i+0] = R
|
|
p[i+1] = G
|
|
p[i+2] = B
|
|
p[i+3] = A
|
|
}
|
|
}
|
|
|
|
func (d *GGDrawer) ClearRectTo(x, y, w, h int, bg color.Color) {
|
|
if w <= 0 || h <= 0 {
|
|
return
|
|
}
|
|
|
|
img, ok := d.DC.Image().(*image.RGBA)
|
|
if !ok || img == nil {
|
|
panic("GGDrawer.ClearRectTo: backing image is not *image.RGBA")
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
x0 := max(x, bounds.Min.X)
|
|
y0 := max(y, bounds.Min.Y)
|
|
x1 := min(x+w, bounds.Max.X)
|
|
y1 := min(y+h, bounds.Max.Y)
|
|
if x0 >= x1 || y0 >= y1 {
|
|
return
|
|
}
|
|
|
|
r, g, b, a := bg.RGBA()
|
|
R := byte(r >> 8)
|
|
G := byte(g >> 8)
|
|
B := byte(b >> 8)
|
|
A := byte(a >> 8)
|
|
|
|
rowBytes := (x1 - x0) * 4
|
|
for yy := y0; yy < y1; yy++ {
|
|
off := yy*img.Stride + x0*4
|
|
for i := 0; i < rowBytes; i += 4 {
|
|
img.Pix[off+i+0] = R
|
|
img.Pix[off+i+1] = G
|
|
img.Pix[off+i+2] = B
|
|
img.Pix[off+i+3] = A
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *GGDrawer) DrawImage(img image.Image, x, y int) {
|
|
g.DC.DrawImage(img, x, y)
|
|
}
|
|
|
|
func (g *GGDrawer) DrawImageScaled(img image.Image, x, y, w, h int) {
|
|
if w <= 0 || h <= 0 {
|
|
return
|
|
}
|
|
b := img.Bounds()
|
|
srcW := b.Dx()
|
|
srcH := b.Dy()
|
|
if srcW <= 0 || srcH <= 0 {
|
|
return
|
|
}
|
|
|
|
g.DC.Push()
|
|
// Translate to destination top-left.
|
|
g.DC.Translate(float64(x), float64(y))
|
|
// Scale so that the source bounds map to (w,h).
|
|
g.DC.Scale(float64(w)/float64(srcW), float64(h)/float64(srcH))
|
|
// Draw at origin in the scaled coordinate system.
|
|
g.DC.DrawImage(img, 0, 0)
|
|
g.DC.Pop()
|
|
}
|