From 9d0db2320722627d1ae93f45152e97e9848e057a Mon Sep 17 00:00:00 2001 From: Jacek Olszak Date: Sun, 27 Jul 2025 13:32:37 +0200 Subject: [PATCH] feat: Use nearest-neighbor scaling in pi.Stretch Because it is more accurate than the current algorithm and the performance is similar. --- _examples/stretchtest/gamepad.png | Bin 0 -> 524 bytes _examples/stretchtest/main.go | 43 +++++++++++++ internal/test/stretch/sprite-1x1.png | Bin 0 -> 211 bytes internal/test/stretch/sprite-2x1.png | Bin 0 -> 212 bytes internal/test/stretch/sprite-3x3.png | Bin 0 -> 223 bytes internal/test/stretch/sprite-3x4.png | Bin 0 -> 223 bytes internal/test/stretch/sprite-3x5.png | Bin 0 -> 223 bytes internal/test/stretch/sprite-3x6.png | Bin 0 -> 223 bytes internal/test/stretch/sprite-4x3.png | Bin 0 -> 226 bytes internal/test/stretch/sprite-5x3.png | Bin 0 -> 229 bytes internal/test/stretch/sprite-6x3.png | Bin 0 -> 234 bytes internal/test/stretch/sprite-6x6.png | Bin 0 -> 232 bytes internal/test/stretch/sprite.png | Bin 0 -> 224 bytes sprite.go | 41 +++++++++---- sprite_test.go | 88 +++++++++++++++++++++++++++ surface_test.go | 2 +- 16 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 _examples/stretchtest/gamepad.png create mode 100644 _examples/stretchtest/main.go create mode 100644 internal/test/stretch/sprite-1x1.png create mode 100644 internal/test/stretch/sprite-2x1.png create mode 100644 internal/test/stretch/sprite-3x3.png create mode 100644 internal/test/stretch/sprite-3x4.png create mode 100644 internal/test/stretch/sprite-3x5.png create mode 100644 internal/test/stretch/sprite-3x6.png create mode 100644 internal/test/stretch/sprite-4x3.png create mode 100644 internal/test/stretch/sprite-5x3.png create mode 100644 internal/test/stretch/sprite-6x3.png create mode 100644 internal/test/stretch/sprite-6x6.png create mode 100644 internal/test/stretch/sprite.png diff --git a/_examples/stretchtest/gamepad.png b/_examples/stretchtest/gamepad.png new file mode 100644 index 0000000000000000000000000000000000000000..43fb08d67777217f1a9ecad459c57df86bf7dcd0 GIT binary patch literal 524 zcmV+n0`vWeP)Px#z)(z7MFs{46c7_T7bSBSEwmsi-##PyY6|(I8~@co|NKDK@?@ObLs`K!6re5| zdOjplI}#uu6f|TgT*M@w-XPY;ru^Ig|Ns8;!1~s>^`MvNYID3wn|V5bKr>S$A~Yf# z8Zs*=aX~w~bw}HzWAxIT+TiN5xzUT0uWfaWN>pMqIYB}sGg~n@j#5&(b!zFgqvPiP zxxf2_o8(xHzDIq9BWF%Bgk)W>jgs8D&G+Ku(yGU>i=c*Ld1qBxPDDaEG&VmOoB#j- z`bk7VR9J=W)XffpFc3yjCqa!Nl3?7hkR`nT8``f7k^X=qGnkNYyMZnaq%E9IM@N}n z7G>1dq8PSOQ+OlSjE&&6?gcHlYuPJ`V7=?un;Q|l;C4QHxA#e0u)A@I_l=BQ2cwWx zxHTGJ!5Ur_icK- zi1&^1E^&i5-n9vg!fEp7%+IYqzn`a7D4H|)-l{vQ!b!agwoG6Xw*7Z zg%R(Y`2Q=1k^KKFhcoLnb-%Br{84*$?ck7gu;9E3)R<*jU4}h6+ORjO7b4XFe$l!B O0000 literal 0 HcmV?d00001 diff --git a/_examples/stretchtest/main.go b/_examples/stretchtest/main.go new file mode 100644 index 00000000..f7269860 --- /dev/null +++ b/_examples/stretchtest/main.go @@ -0,0 +1,43 @@ +// Copyright 2025 Jacek Olszak +// This code is licensed under MIT license (see LICENSE for details) + +package main + +import ( + _ "embed" + "github.com/elgopher/pi" + "github.com/elgopher/pi/piebiten" + "github.com/elgopher/pi/pikey" + "github.com/elgopher/pi/piscope" +) + +//go:embed "gamepad.png" +var gamepadPNG []byte + +func main() { + pi.SetScreenSize(100, 60) + pi.Palette = pi.DecodePalette(gamepadPNG) + canvas := pi.DecodeCanvas(gamepadPNG) + spr := pi.SpriteFrom(canvas, 58, 26, 9, 9) + piscope.Start() + posx := 0 + posy := 0 + pi.Update = func() { + if pikey.Duration(pikey.Left) > 0 { + posx-- + } + if pikey.Duration(pikey.Right) > 0 { + posx++ + } + if pikey.Duration(pikey.Up) > 0 { + posy-- + } + if pikey.Duration(pikey.Down) > 0 { + posy++ + } + } + pi.Draw = func() { + pi.DrawSprite(spr, posx, posy) + } + piebiten.Run() +} diff --git a/internal/test/stretch/sprite-1x1.png b/internal/test/stretch/sprite-1x1.png new file mode 100644 index 0000000000000000000000000000000000000000..ce898bb86842b0229342a6efff4ec0472449bb37 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N yw6y)d$8h{XmO0Q$eoq(25RRG22@TAWY(Ph|ePNK*o_Lxck44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$0Z$jl5RRG22@TAVjBKA68Mr<&=&h9ESORj7r>mdKI;Vst04T*z A@&Et; literal 0 HcmV?d00001 diff --git a/internal/test/stretch/sprite-3x3.png b/internal/test/stretch/sprite-3x3.png new file mode 100644 index 0000000000000000000000000000000000000000..3264241a6159e766e4170adaa87cdeb66c0a229f GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$X-^l&5RRG22@T9@nT{OJVQvhE0>Y-GCk44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$X-^l&5RRG22@T9@nT{OJVQvh)3)g6zY>;PQFkH?k*WLPVAINE* Lu6{1-oD!M<(t=dx literal 0 HcmV?d00001 diff --git a/internal/test/stretch/sprite-3x5.png b/internal/test/stretch/sprite-3x5.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d5d2bd083e425e03cd66933550761d3536ac7e GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$X-^l&5RRG22@T9@nT{rHYkC-37Ov6gJm$d8VDg4B;bD4P7|3a! Lu6{1-oD!M<`F2(a literal 0 HcmV?d00001 diff --git a/internal/test/stretch/sprite-3x6.png b/internal/test/stretch/sprite-3x6.png new file mode 100644 index 0000000000000000000000000000000000000000..c8e35fb00eb7129bd3e8e5fce3aa4f61435a4afd GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$X-^l&5RRG22@T9@nT{rHYkC-@oFfB;dKp<6%x^HRluX+Kaz2Bn LtDnm{r-UW|yk44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$IZqeI5RRG22@T9@nVF6T&S7qD3}*tuLZ+lB@G&r0E@jl!6K>rK Oa-FBEpUXO@geCyBTvQeS literal 0 HcmV?d00001 diff --git a/internal/test/stretch/sprite-5x3.png b/internal/test/stretch/sprite-5x3.png new file mode 100644 index 0000000000000000000000000000000000000000..b87d52a81c09c5cda0b5d0493692af971e56a248 GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$MNb#U5RRG22@T9@YMG9X0?uJ!ZVn7b0>Z+kOi57ydc*DoW8jml RHf4|lJzf1=);T3K0RX*-Rk;8F literal 0 HcmV?d00001 diff --git a/internal/test/stretch/sprite-6x3.png b/internal/test/stretch/sprite-6x3.png new file mode 100644 index 0000000000000000000000000000000000000000..c983631384068472d9f9bbc500f4f9634395d074 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$HBT4E5RRG22@T9@YMGgij%;jfk&%*;jEq1=YO1TNrzcwy1B1U9 VFMg{vd$@?2>=g}RB`|S literal 0 HcmV?d00001 diff --git a/internal/test/stretch/sprite-6x6.png b/internal/test/stretch/sprite-6x6.png new file mode 100644 index 0000000000000000000000000000000000000000..e17effd0c0135ee936b6f36377def20b760442e5 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`6b1N%xB_V) z_`f!}*^uG?|Nq~QEUo@;z|hQ)x@0y3!@vKnEmN$lo&gm+KYDIXYfH9fv|;qpY{QhL z|GRtc1qbykUAi19bs014FGR&_F9I4W_{V4F8=N7?Y1N zw6y)d$8h{XmO0Q$6;Bt(5RRG22@T9@YMGgijwx+x*7Wo=Fc>*UMg|533C(0=WyqSq U9GLMzopr0P~Vp=Kufz literal 0 HcmV?d00001 diff --git a/internal/test/stretch/sprite.png b/internal/test/stretch/sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..a973bea55e411011317026b0f1f668c1ff0ec47d GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$63?z4LymlQ(F%}28J29*~C-V}>DGKljaRt&q z@PBP`vmwL(|Np-qSz7(yfT5Wob;)c7hJXKCTc%i9Jp(Fue)Qa&)|PC|Xv65G*@h`g z|9AJ?3l8d8x^!t!P|%bqQ~phvGI`3s|Noc%2kHW128LQspn+Ca8cc!z8U8ylFeV>k zXleU@kKy=(EOVfhGM+AuAsjQ46B-!QG9B62*diqvfn=(y=L!u5?h1yhHL(``Ah&tC L`njxgN@xNA47pNy literal 0 HcmV?d00001 diff --git a/sprite.go b/sprite.go index 47737999..a6c78fcb 100644 --- a/sprite.go +++ b/sprite.go @@ -5,6 +5,7 @@ package pi import ( "fmt" + "github.com/elgopher/pi/pimath" ) // DrawSprite draws the given sprite at (dx, dy) on the current draw target. @@ -72,31 +73,45 @@ func Stretch(sprite Sprite, dx, dy, dw, dh int) { targetStride := drawTarget.width - int(dst.W) srcSource := sprite.Source - if sprite.FlipY { - src.Y += float64(dh-1) * stepY - stepY *= -1 - } + stepXAbs := src.W / dst.W + stepYAbs := src.H / dst.H + // start sampling from half-step offsets if sprite.FlipX { - src.X += float64(dw-1) * stepX - stepX *= -1 + src.X += src.W - stepXAbs/2 + stepXAbs = -stepXAbs + } else { + src.X += stepXAbs / 2 } - srcX, srcY := src.X, src.Y + if sprite.FlipY { + src.Y += src.H - stepYAbs/2 + stepYAbs = -stepYAbs + } else { + src.Y += stepYAbs / 2 + } + + srcY := src.Y + + srcMaxX := int(src.X + src.W) + srcMaxY := int(src.Y + src.H) for line := 0.0; line < dst.H; line++ { - srcLineIdx := int(srcY) * srcSource.width // multiplication, but only once per line, so it's not a performance problem + syIndex := pimath.Clamp(int(srcY), 0, srcMaxY-1) + srcLineIdx := syIndex * srcSource.width + + srcX := src.X + for cell := 0; cell < int(dst.W); cell++ { + sxIndex := pimath.Clamp(int(srcX), 0, srcMaxX-1) - for cell := 0.0; cell < dst.W; cell++ { - sourceColor := srcSource.data[srcLineIdx+int(srcX)] & ReadMask + sourceColor := srcSource.data[srcLineIdx+sxIndex] & ReadMask targetColor := drawTarget.data[targetIdx] & TargetMask drawTarget.data[targetIdx] = ColorTables[(sourceColor|targetColor)>>6][sourceColor&(MaxColors-1)][targetColor&(MaxColors-1)] - srcX += stepX + srcX += stepXAbs targetIdx++ } - srcX = src.X - srcY += stepY + srcY += stepYAbs targetIdx += targetStride } } diff --git a/sprite_test.go b/sprite_test.go index e34acd35..3d289f1e 100644 --- a/sprite_test.go +++ b/sprite_test.go @@ -4,12 +4,100 @@ package pi_test import ( + _ "embed" + "github.com/elgopher/pi/pitest" "testing" "github.com/elgopher/pi" ) +var ( + //go:embed internal/test/stretch/sprite.png + spritePNG []byte + //go:embed internal/test/stretch/sprite-3x3.png + sprite3x3PNG []byte + //go:embed internal/test/stretch/sprite-6x6.png + sprite6x6PNG []byte + //go:embed internal/test/stretch/sprite-6x3.png + sprite6x3PNG []byte + //go:embed internal/test/stretch/sprite-3x6.png + sprite3x6PNG []byte + //go:embed internal/test/stretch/sprite-4x3.png + sprite4x3PNG []byte + //go:embed internal/test/stretch/sprite-5x3.png + sprite5x3PNG []byte + //go:embed internal/test/stretch/sprite-3x4.png + sprite3x4PNG []byte + //go:embed internal/test/stretch/sprite-3x5.png + sprite3x5PNG []byte + //go:embed internal/test/stretch/sprite-1x1.png + sprite1x1PNG []byte + //go:embed internal/test/stretch/sprite-2x1.png + sprite2x1PNG []byte +) + func TestStretch(t *testing.T) { + t.Run("inside screen", func(t *testing.T) { + tests := map[string]struct { + dw, dh int + png []byte + }{ + "3x3": { + dw: 3, dh: 3, + png: sprite3x3PNG, + }, + "6x6": { + dw: 6, dh: 6, + png: sprite6x6PNG, + }, + "6x3": { + dw: 6, dh: 3, + png: sprite6x3PNG, + }, + "3x6": { + dw: 3, dh: 6, + png: sprite3x6PNG, + }, + "4x3": { + dw: 4, dh: 3, + png: sprite4x3PNG, + }, + "5x3": { + dw: 5, dh: 3, + png: sprite5x3PNG, + }, + "3x4": { + dw: 3, dh: 4, + png: sprite3x4PNG, + }, + "3x5": { + dw: 3, dh: 5, + png: sprite3x5PNG, + }, + "1x1": { + dw: 1, dh: 1, + png: sprite1x1PNG, + }, + "2x1": { + dw: 2, dh: 1, + png: sprite2x1PNG, + }, + } + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + pi.SetScreenSize(8, 8) + pi.Cls() + pi.Palette = pi.DecodePalette(spritePNG) + sprite := pi.SpriteFrom(pi.DecodeCanvas(spritePNG), 1, 1, 3, 3) + // when + pi.Stretch(sprite, 1, 1, testCase.dw, testCase.dh) + // then + expected := pi.DecodeCanvas(testCase.png) + pitest.AssertSurfaceEqual(t, expected, pi.Screen()) + }) + } + }) + // temporary test dst := pi.NewCanvas(16, 16) pi.SetDrawTarget(dst) diff --git a/surface_test.go b/surface_test.go index 97734dae..92a12da1 100644 --- a/surface_test.go +++ b/surface_test.go @@ -375,7 +375,7 @@ func BenchmarkDrawCanvas(b *testing.B) { src.Clear(7) for b.Loop() { - pi.DrawCanvas(src, 130, 130) + pi.DrawCanvas(src, 130, 130) // 2256 ns/op } }