Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion ext/text/atlas.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package text

import (
"image"
"image/color"
"image/draw"
"sort"
"unicode"
Expand All @@ -24,7 +25,7 @@ type Glyph struct {
// Atlas is a set of pre-drawn glyphs of a fixed set of runes. This allows for efficient text drawing.
type Atlas struct {
face font.Face
pic pixel.Picture
pic *pixel.PictureData
mapping map[rune]Glyph
ascent float64
descent float64
Expand Down Expand Up @@ -104,6 +105,59 @@ func (a *Atlas) Picture() pixel.Picture {
return a.pic
}

// PictureDataCopy returns a deep copy of the atlas's underlying pixel data. The caller
// owns the returned PictureData and may modify it without affecting the Atlas.
func (a *Atlas) PictureDataCopy() *pixel.PictureData {
newPic := &pixel.PictureData{
Stride: a.pic.Stride,
Rect: a.pic.Rect,
Pix: make([]color.RGBA, len(a.pic.Pix)),
}
copy(newPic.Pix, a.pic.Pix)
return newPic
}

// CloneWithPictureData returns a new Atlas that uses pic as its backing image.
// frame is the sub-rectangle of pic where the atlas glyphs live; it must have
// the same width and height as the original atlas picture. All glyph coordinates
// are translated so they remain correct relative to the new location in pic.
//
// The intended use is to blit PictureDataCopy into a larger shared atlas image and
// then call CloneWithPictureData so text can share a pixel.Batch with other sprites.
func (a *Atlas) CloneWithPictureData(pic *pixel.PictureData, frame pixel.Rect) *Atlas {
if a.pic.Bounds().W() != frame.W() || a.pic.Bounds().H() != frame.H() {
panic("atlas: new frame dimensions do not match prior picture")
}
if !pic.Bounds().Contains(frame.Min) || !pic.Bounds().Contains(frame.Max) {
panic("atlas: new frame is out of bounds of supplied pic")
}
newAtlas := &Atlas{
face: a.face,
pic: pic,
mapping: make(map[rune]Glyph, len(a.mapping)),
ascent: a.ascent,
descent: a.descent,
lineHeight: a.lineHeight,
}
// account for non (0,0) origin images
picFrameDelta := frame.Min.Sub(a.pic.Rect.Min)
// for each glyph, translate the dot and frame to account for the new frame location within the supplied pic
for r, glyph := range a.mapping {
rMin := glyph.Frame.Min.Add(picFrameDelta)
rMax := rMin.Add(pixel.V(glyph.Frame.W(), glyph.Frame.H()))
newFrame := pixel.Rect{
Min: rMin,
Max: rMax,
}
newAtlas.mapping[r] = Glyph{
Dot: glyph.Dot.Add(picFrameDelta),
Frame: newFrame,
Advance: glyph.Advance,
}
}
return newAtlas
}

// Contains reports wheter r in contained within the Atlas.
func (a *Atlas) Contains(r rune) bool {
_, ok := a.mapping[r]
Expand Down
114 changes: 114 additions & 0 deletions ext/text/atlas_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package text_test

import (
"image/color"
"testing"

"github.com/gopxl/pixel/v2"
"github.com/gopxl/pixel/v2/ext/text"
"golang.org/x/image/font/inconsolata"
)
Expand All @@ -27,3 +29,115 @@ func TestAtlas7x13(t *testing.T) {
func TestAtlasInconsolata(t *testing.T) {
text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
}

func TestAtlasPictureDataCopy(t *testing.T) {
a := text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
orig := a.Picture().(*pixel.PictureData)

cp := a.PictureDataCopy()
if cp == orig {
t.Fatal("PictureDataCopy returned the same pointer as the original")
}
if cp.Stride != orig.Stride {
t.Errorf("Stride mismatch: got %d, want %d", cp.Stride, orig.Stride)
}
if cp.Rect != orig.Rect {
t.Errorf("Rect mismatch: got %v, want %v", cp.Rect, orig.Rect)
}
if len(cp.Pix) != len(orig.Pix) {
t.Fatalf("Pix length mismatch: got %d, want %d", len(cp.Pix), len(orig.Pix))
}
// Verify deep copy: mutating the copy does not affect the original.
if len(cp.Pix) > 0 {
origPix := orig.Pix[0]
cp.Pix[0] = color.RGBA{R: ^origPix.R, G: ^origPix.G, B: ^origPix.B, A: ^origPix.A}
if orig.Pix[0] != origPix {
t.Error("PictureDataCopy is not a deep copy: mutating the copy affected the original")
}
}
}

func TestAtlasCloneWithPictureData(t *testing.T) {
a := text.NewAtlas(inconsolata.Regular8x16, text.ASCII)
tests := []struct {
name string
offset pixel.Vec
shouldPanic bool
panicMessage string
}{
{
name: "identity (frame == original bounds)",
offset: pixel.ZV,
},
{
name: "translated into larger shared picture",
offset: pixel.V(50, 30),
},
{
name: "wrong frame size panics",
shouldPanic: true,
panicMessage: "atlas: new frame dimensions do not match prior picture",
},
}

origBounds := a.Picture().Bounds()
origW, origH := origBounds.W(), origBounds.H()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.shouldPanic {
defer func() {
r := recover()
if r == nil {
t.Fatal("expected panic, got none")
}
if msg, ok := r.(string); !ok || msg != tt.panicMessage {
t.Errorf("wrong panic: %v", r)
}
}()
// Supply a frame with wrong dimensions.
wrongPic := pixel.MakePictureData(pixel.R(0, 0, origW+1, origH))
wrongFrame := pixel.R(0, 0, origW+1, origH)
a.CloneWithPictureData(wrongPic, wrongFrame)
return
}

// Build a shared picture large enough to hold the atlas at the given offset.
sharedW := origW + tt.offset.X
sharedH := origH + tt.offset.Y
sharedPic := pixel.MakePictureData(pixel.R(0, 0, sharedW, sharedH))
frame := pixel.R(tt.offset.X, tt.offset.Y, tt.offset.X+origW, tt.offset.Y+origH)

clone := a.CloneWithPictureData(sharedPic, frame)

// The clone must contain every rune the original does.
for _, r := range text.ASCII {
if !clone.Contains(r) {
t.Errorf("clone does not contain rune %q", r)
}
}

// Glyph coordinates must be shifted by the frame offset relative to
// the original atlas origin.
delta := frame.Min.Sub(origBounds.Min)
for _, r := range text.ASCII {
if !a.Contains(r) {
continue
}
origGlyph := a.Glyph(r)
cloneGlyph := clone.Glyph(r)
wantDot := origGlyph.Dot.Add(delta)
if cloneGlyph.Dot != wantDot {
t.Errorf("rune %q: Dot got %v, want %v", r, cloneGlyph.Dot, wantDot)
}
wantFrame := origGlyph.Frame.Moved(delta)
if cloneGlyph.Frame != wantFrame {
t.Errorf("rune %q: Frame got %v, want %v", r, cloneGlyph.Frame, wantFrame)
}
if cloneGlyph.Advance != origGlyph.Advance {
t.Errorf("rune %q: Advance got %v, want %v", r, cloneGlyph.Advance, origGlyph.Advance)
}
}
})
}
}