Skip to content

Commit 5cef81a

Browse files
committed
Add PixMap struct
PixMap is a generic data structure for manipulating any kind of pixel data - screen, sprite-sheet etc. PixMap uses a single byte (8 bits) for storing single color/pixel. This means that max 256 colors can be used. PixMap can also be used for maps which not necessary contain pixel colors, such as world map (as long as only 256 different tiles are used). This commit replaces pi.Screen and pi.SpriteSheet with pi.PixMap. PixMap provides also new methods: * Copy - copy PixMap fragment to another PixMap at specific location * Merge - customizable variant of Copy. User can provide his own merge function which will be used instead of directly copying pixels. * Foreach - run code line by line on selected PixMap fragment * Pointer - low-level method for high-performance processing
1 parent 0cc8f5d commit 5cef81a

27 files changed

+1478
-629
lines changed

devtools/internal/inspector/draw.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func Draw() {
3737
func cursorOutOfWindow() bool {
3838
x, y := pi.MousePos()
3939
screen := pi.Scr()
40-
return x < 0 || x >= screen.W || y < 0 || y >= screen.H
40+
return x < 0 || x >= screen.Width() || y < 0 || y >= screen.Height()
4141
}
4242

4343
func handleScreenshot() {

devtools/internal/inspector/inspector.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func moveBarIfNeeded() {
1414
switch {
1515
case isBarOnTop && mouseY <= 12:
1616
isBarOnTop = false
17-
case !isBarOnTop && mouseY >= pi.Scr().H-12:
17+
case !isBarOnTop && mouseY >= pi.Scr().Height()-12:
1818
isBarOnTop = true
1919
}
2020
}

devtools/internal/inspector/measure.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ func (m *Measure) drawBar() {
2929
mouseX, mouseY := pi.MousePos()
3030
var barY int
3131
if !isBarOnTop {
32-
barY = screen.H - 7
32+
barY = screen.Height() - 7
3333
}
3434

35-
pi.RectFill(0, barY, screen.W, barY+6, BgColor)
35+
pi.RectFill(0, barY, screen.Width(), barY+6, BgColor)
3636

3737
textX := 1
3838
textY := barY + 1

devtools/internal/inspector/toolbar.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ func (t *Toolbar) toggle() {
2828
t.pos.X = 0
2929
}
3030
scr := pi.Scr()
31-
if t.pos.X+toolbarWidth > scr.W {
32-
t.pos.X = scr.W - toolbarWidth - 1
31+
if t.pos.X+toolbarWidth > scr.Width() {
32+
t.pos.X = scr.Width() - toolbarWidth - 1
3333
}
3434
t.pos.Y -= toolbarHeight + 2
3535
if t.pos.Y < 0 {

devtools/internal/snapshot/snapshot.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ func Take() {
2323
}
2424

2525
screen := pi.Scr()
26+
pix := screen.Pix()
2627
if snapshot == nil {
27-
snapshot = make([]byte, len(screen.Pix))
28+
snapshot = make([]byte, len(pix))
2829
}
29-
copy(snapshot, screen.Pix)
30+
copy(snapshot, pix)
3031
}
3132

3233
func Draw() {
33-
copy(pi.Scr().Pix, snapshot)
34+
copy(pi.Scr().Pix(), snapshot)
3435
}
3536

3637
func Undo() {

ebitengine/ebitengine.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ func Run() error {
3030
ebiten.SetScreenClearedEveryFrame(false)
3131
ebiten.SetRunnableOnUnfocused(true)
3232
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
33-
ebiten.SetWindowSize(screen.W*scale(), screen.H*scale())
34-
ebiten.SetWindowSizeLimits(screen.W, screen.H, -1, -1)
33+
ebiten.SetWindowSize(screen.Width()*scale(), screen.Height()*scale())
34+
ebiten.SetWindowSizeLimits(screen.Width(), screen.Height(), -1, -1)
3535
ebiten.SetCursorMode(ebiten.CursorModeHidden)
3636
ebiten.SetWindowTitle("Pi Game")
3737

ebitengine/game.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (e *game) Draw(screen *ebiten.Image) {
7676
}
7777

7878
func (e *game) writeScreenPixels(screen *ebiten.Image) {
79-
pix := pi.Scr().Pix
79+
pix := pi.Scr().Pix()
8080
if e.screenDataRGBA == nil || len(e.screenDataRGBA)/4 != len(pix) {
8181
e.screenDataRGBA = make([]byte, len(pix)*4)
8282
}
@@ -96,5 +96,5 @@ func (e *game) writeScreenPixels(screen *ebiten.Image) {
9696

9797
func (e *game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
9898
scr := pi.Scr()
99-
return scr.W, scr.H
99+
return scr.Width(), scr.Height()
100100
}

examples/memory/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
func main() {
1313
pi.Draw = func() {
14-
pixels := pi.Scr().Pix
14+
pixels := pi.Scr().Pix()
1515
for i := 0; i < len(pixels); i++ {
1616
randomColor := byte(rand.Intn(16))
1717
pixels[i] = randomColor // put a random color to each pixel

examples/pixmap/main.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Example showing how to use PixMap struct, which is used to store screen
2+
// and sprite-sheet pixels.
3+
package main
4+
5+
import (
6+
"embed"
7+
8+
"github.com/elgopher/pi"
9+
"github.com/elgopher/pi/ebitengine"
10+
)
11+
12+
//go:embed sprite-sheet.png
13+
var resources embed.FS
14+
15+
func main() {
16+
pi.Load(resources)
17+
18+
// copy from sprite-sheet to sprite-sheet:
19+
pi.SprSheet().Copy(10, 0, 100, 100, pi.SprSheet(), 0, 0)
20+
21+
// draw a filled rectangle directly to sprite-sheet:
22+
pi.SprSheet().RectFill(60, 30, 70, 40, 7)
23+
24+
// merge from sprite-sheet to screen using custom merge function, which merges two lines
25+
pi.SprSheet().Merge(-1, -1, 103, 70, pi.Scr(), -1, -1, func(dst, src []byte) {
26+
for x := 0; x < len(dst); x++ {
27+
dst[x] += pi.DrawPalette[src[x]] + 1
28+
}
29+
})
30+
31+
// update each line in a loop:
32+
pi.Scr().Foreach(10, 10, 16, 16, func(x, y int, line []byte) {
33+
for i := 0; i < len(line); i++ {
34+
line[i] = byte(i)
35+
}
36+
})
37+
38+
ebitengine.MustRun()
39+
}

examples/pixmap/sprite-sheet.png

268 Bytes
Loading

internal/bench/pixmap_bench_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// (c) 2022 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package bench_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/elgopher/pi"
10+
)
11+
12+
func BenchmarkPixMapPointer(b *testing.B) {
13+
pixMap := pi.NewPixMap(3, 2) // 3x2
14+
15+
var ptr pi.Pointer
16+
var ok bool
17+
18+
b.ResetTimer()
19+
b.ReportAllocs()
20+
21+
for i := 0; i < b.N; i++ {
22+
ptr, ok = pixMap.Pointer(1, 1, 2, 1)
23+
}
24+
_ = ptr
25+
_ = ok
26+
}
27+
28+
func BenchmarkCopy(b *testing.B) {
29+
runBenchmarks(b, func(res Resolution) {
30+
for i := 0; i < 100; i++ {
31+
pi.SprSheet().Copy(0, 0, 16, 16, pi.Scr(), 16, 16) // 2x times faster than SprSize
32+
}
33+
})
34+
}
35+
36+
func SrcAtop(dst, src []byte) { copy(dst, src) }
37+
38+
func BenchmarkMerge(b *testing.B) {
39+
runBenchmarks(b, func(res Resolution) {
40+
for i := 0; i < 100; i++ {
41+
pi.SprSheet().Merge(0, 0, 16, 16, pi.Scr(), 16, 16, SrcAtop) // FAST! NOT AS FAST AS COPY BUT THE PERF IS GREAT!
42+
}
43+
})
44+
}
45+
46+
func BenchmarkForeach(b *testing.B) {
47+
src := make([]byte, 16)
48+
runBenchmarks(b, func(res Resolution) {
49+
for i := 0; i < 100; i++ {
50+
pi.Scr().Foreach(0, 0, 16, 16, func(x, y int, dst []byte) { copy(dst, src) })
51+
}
52+
})
53+
}

internal/fuzz/pixmap_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// (c) 2022 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
//go:build !js
5+
6+
package fuzz_test
7+
8+
import (
9+
"testing"
10+
11+
"github.com/elgopher/pi"
12+
)
13+
14+
func FuzzPixMap_Pointer(f *testing.F) {
15+
pixMap := pi.NewPixMap(8, 9).WithClip(1, 1, 3, 5)
16+
17+
f.Fuzz(func(t *testing.T, x, y, w, h int) {
18+
pixMap.Pointer(x, y, w, h)
19+
})
20+
}
21+
22+
func FuzzPixMap_Foreach(f *testing.F) {
23+
pixMap := pi.NewPixMap(2, 3)
24+
f.Fuzz(func(t *testing.T, x, y, w, h, dstX, dstY int) {
25+
pixMap.Foreach(x, y, w, h, func(x, y int, dst []byte) {
26+
dst[0] = byte(x + y)
27+
})
28+
})
29+
}
30+
31+
func FuzzPixMap_Copy_Src_Bigger(f *testing.F) {
32+
src := pi.NewPixMap(5, 4)
33+
dst := pi.NewPixMap(2, 3)
34+
35+
f.Fuzz(func(t *testing.T, x, y, w, h, dstX, dstY int) {
36+
src.Copy(x, y, w, h, dst, dstX, dstY)
37+
})
38+
}
39+
40+
func FuzzPixMap_Copy_Dst_Bigger(f *testing.F) {
41+
src := pi.NewPixMap(2, 3)
42+
dst := pi.NewPixMap(5, 4)
43+
44+
f.Fuzz(func(t *testing.T, x, y, w, h, dstX, dstY int) {
45+
src.Copy(x, y, w, h, dst, dstX, dstY)
46+
})
47+
}
48+
49+
func FuzzPixMap_Merge(f *testing.F) {
50+
src := pi.NewPixMap(2, 3)
51+
dst := pi.NewPixMap(4, 3)
52+
53+
f.Fuzz(func(t *testing.T, x, y, w, h, dstX, dstY int) {
54+
src.Merge(x, y, w, h, dst, dstX, dstY, func(dst, src []byte) {
55+
copy(dst, src)
56+
})
57+
})
58+
}

internal/fuzz/screen_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,17 @@ import (
1313

1414
const color = 7
1515

16-
func FuzzPset(f *testing.F) {
17-
pi.Reset()
18-
pi.SetScreenSize(16, 16)
16+
func FuzzPixMap_Set(f *testing.F) {
17+
pixMap := pi.NewPixMap(16, 16)
1918
f.Fuzz(func(t *testing.T, x, y int) {
20-
pi.Pset(x, y, color)
19+
pixMap.Set(x, y, color)
2120
})
2221
}
2322

24-
func FuzzPget(f *testing.F) {
25-
pi.Reset()
26-
pi.SetScreenSize(16, 16)
23+
func FuzzPixMap_Get(f *testing.F) {
24+
pixMap := pi.NewPixMap(16, 16)
2725
f.Fuzz(func(t *testing.T, x, y int) {
28-
pi.Pget(x, y)
26+
pixMap.Get(x, y)
2927
})
3028
}
3129

pi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func Reset() {
8383
systemFont.Data, _ = font.Load(systemFontPNG)
8484
customFont = defaultCustomFont
8585
customFont.Data = make([]byte, fontDataSize)
86-
screen = newScreen(defaultScreenWidth, defaultScreenHeight)
86+
screen = NewPixMap(defaultScreenWidth, defaultScreenHeight)
8787
sprSheet = newSpriteSheet(defaultSpriteSheetWidth, defaultSpriteSheetHeight)
8888
Palette = defaultPalette
8989
}

pi_test.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ func TestReset(t *testing.T) {
2525
pi.Reset()
2626
// then
2727
sprSheet := pi.SprSheet()
28-
assert.Equal(t, 128, sprSheet.W)
29-
assert.Equal(t, 128, sprSheet.H)
30-
assert.Equal(t, make([]byte, 16384), sprSheet.Pix)
28+
assert.Equal(t, 128, sprSheet.Width())
29+
assert.Equal(t, 128, sprSheet.Height())
30+
assert.Equal(t, make([]byte, 16384), sprSheet.Pix())
3131
})
3232

3333
t.Run("should reset screen", func(t *testing.T) {
@@ -36,11 +36,11 @@ func TestReset(t *testing.T) {
3636
pi.Reset()
3737
// then
3838
scr := pi.Scr()
39-
assert.Equal(t, 128, scr.W)
40-
assert.Equal(t, 128, scr.H)
41-
assert.Equal(t, make([]byte, 16384), scr.Pix)
42-
assert.Zero(t, scr.Camera)
43-
assert.Equal(t, pi.Region{W: 128, H: 128}, scr.Clip)
39+
assert.Equal(t, 128, scr.Width())
40+
assert.Equal(t, 128, scr.Height())
41+
assert.Equal(t, make([]byte, 16384), scr.Pix())
42+
assert.Zero(t, pi.ScreenCamera)
43+
assert.Equal(t, pi.Region{W: 128, H: 128}, scr.Clip())
4444
})
4545

4646
t.Run("should reset palette", func(t *testing.T) {
@@ -112,7 +112,7 @@ func TestSetScreenSize(t *testing.T) {
112112
t.Run(strconv.Itoa(size), func(t *testing.T) {
113113
pi.Reset()
114114
assert.Panics(t, func() {
115-
pi.SetScreenSize(size, pi.Scr().H)
115+
pi.SetScreenSize(size, pi.Scr().Height())
116116
})
117117
})
118118
}
@@ -123,7 +123,7 @@ func TestSetScreenSize(t *testing.T) {
123123
t.Run(strconv.Itoa(size), func(t *testing.T) {
124124
pi.Reset()
125125
assert.Panics(t, func() {
126-
pi.SetScreenSize(pi.Scr().W, size)
126+
pi.SetScreenSize(pi.Scr().Width(), size)
127127
})
128128
})
129129
}
@@ -134,7 +134,7 @@ func TestSetScreenSize(t *testing.T) {
134134
// when
135135
pi.SetScreenSize(2, 3)
136136
// then
137-
assert.Equal(t, make([]byte, 6), pi.Scr().Pix)
137+
assert.Equal(t, make([]byte, 6), pi.Scr().Pix())
138138
})
139139
}
140140

@@ -148,10 +148,10 @@ func TestLoad(t *testing.T) {
148148
"sprite-sheet.png": &fstest.MapFile{Data: spriteSheet16x16},
149149
})
150150
// then
151-
assert.Equal(t, 16, pi.SprSheet().W)
152-
assert.Equal(t, 16, pi.SprSheet().H)
151+
assert.Equal(t, 16, pi.SprSheet().Width())
152+
assert.Equal(t, 16, pi.SprSheet().Height())
153153
img := decodePNG(t, "internal/testimage/sprite-sheet-16x16.png")
154-
assert.Equal(t, img.Pixels, pi.SprSheet().Pix)
154+
assert.Equal(t, img.Pixels, pi.SprSheet().Pix())
155155
assert.Equal(t, img.Palette, pi.Palette)
156156
})
157157

@@ -179,7 +179,7 @@ func TestLoad(t *testing.T) {
179179
})
180180
})
181181

182-
t.Run("should panic if sprite-sheet size is not multiplication of 8", func(t *testing.T) {
182+
t.Run("should panic if sprite-sheet size is not multiple of 8", func(t *testing.T) {
183183
assert.Panics(t, func() {
184184
pi.Load(fstest.MapFS{
185185
"sprite-sheet.png": &fstest.MapFile{Data: invalidSpriteSheetWidth},

0 commit comments

Comments
 (0)