pix_test.gno
7.29 Kb · 198 lines
1package art
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/uassert/v0"
8)
9
10// sprite builds a small indexed bitmap from a rune-per-pixel literal, so a test
11// can show the input instead of listing indices.
12func sprite(pal Palette, rows ...string) *Pix {
13 w := 0
14 for _, r := range rows {
15 if n := len([]rune(r)); n > w {
16 w = n
17 }
18 }
19 p := NewPix(w, len(rows), pal)
20 for y, row := range rows {
21 for x, r := range []rune(row) {
22 if r == '.' {
23 continue
24 }
25 p.Set(x, y, uint8(r-'0'))
26 }
27 }
28 return p
29}
30
31// TestRampCollapsesWhatGlyphsKeepsApart is the finding the whole package turns
32// on: a luminance ramp is the wrong conversion for indexed art.
33//
34// Sand (#e8dcc8) and bone (#d9d9d9) are two distinct palette entries any sprite
35// might carry. Their luminances are 221 and 217, four steps apart out of 255,
36// so a ten-step ramp drops both into bucket 8 and the shape between them
37// disappears. Keying on the palette index instead cannot do that, because the
38// index IS the artist's own segmentation.
39func TestRampCollapsesWhatGlyphsKeepsApart(t *testing.T) {
40 pal := Palette{"#000000", "#e8dcc8", "#d9d9d9"}
41 uassert.Equal(t, 221, Luminance(0xe8dcc8), "sand")
42 uassert.Equal(t, 217, Luminance(0xd9d9d9), "bone")
43
44 p := sprite(pal, "0112")
45
46 ramp := p.CanvasOpts(Opts{Mode: Ramp}).Text()
47 glyph := p.CanvasOpts(Opts{Mode: Glyphs}).Text()
48
49 // Both non-background entries land on the same ramp rune, so the two
50 // regions merge into one blob.
51 uassert.Equal(t, " %%%", ramp, "ramp merges sand and bone")
52 uassert.Equal(t, 2, distinctRunes(ramp), "ramp shows two distinct runes")
53
54 // Glyphs keeps three, one per palette index.
55 uassert.Equal(t, " ##@", glyph, "glyphs keep every index apart")
56 uassert.Equal(t, 3, distinctRunes(glyph), "glyphs show three distinct runes")
57}
58
59func TestGlyphDefaultsAreWideAndMonochrome(t *testing.T) {
60 p := sprite(Palette{"", "#ff0000"}, "01")
61
62 // A terminal cell is about twice as tall as it is wide, so one cell per
63 // pixel would squash a sprite to half height.
64 wide := p.Canvas(Glyphs)
65 uassert.Equal(t, 4, wide.W, "default doubles the width")
66 uassert.Equal(t, " ##", wide.Text(), "each pixel is two cells")
67 uassert.Equal(t, Default, wide.At(2, 0).FG, "no colour by default")
68
69 narrow := p.CanvasOpts(Opts{Mode: Glyphs, Color: true})
70 uassert.Equal(t, 2, narrow.W, "Wide off means one cell per pixel")
71 uassert.Equal(t, 0xff0000, narrow.At(1, 0).FG, "Color on carries the palette through")
72}
73
74func TestCustomGlyphsAndWrapping(t *testing.T) {
75 p := sprite(Palette{"", "", ""}, "012")
76 got := p.CanvasOpts(Opts{Mode: Glyphs, Glyphs: []rune{'a', 'b'}}).Text()
77 // Two glyphs for three indices: index 2 wraps back onto 'a'. A real
78 // collision, which is why the doc tells you to pass your own set.
79 uassert.Equal(t, "aba", got, "a short glyph set wraps")
80}
81
82func TestHalfBlock(t *testing.T) {
83 pal := Palette{"", "#ff0000", "#0000ff"}
84 p := sprite(pal, "11", "22", "11")
85
86 c := p.Canvas(HalfBlock)
87 uassert.Equal(t, 2, c.W, "width is unchanged")
88 uassert.Equal(t, 2, c.H, "three rows pack into two cells")
89
90 top := c.At(0, 0)
91 uassert.Equal(t, '▀', top.R, "upper half block")
92 uassert.Equal(t, 0xff0000, top.FG, "foreground is the top pixel")
93 uassert.Equal(t, 0x0000ff, top.BG, "background is the bottom pixel")
94
95 // The odd last row has no partner, so its background falls to Default
96 // rather than reading a pixel that is not there.
97 uassert.Equal(t, Default, c.At(0, 1).BG, "odd height leaves the last background unset")
98}
99
100func TestHalfBlockMonochromeFallsBackToInk(t *testing.T) {
101 p := sprite(Palette{"", "#ffffff"}, "10", "01")
102 // Without colour, '▀' everywhere would say nothing at all, so the mode
103 // reports which halves carry ink instead.
104 uassert.Equal(t, "▀▄", p.CanvasOpts(Opts{Mode: HalfBlock}).Text(), "ink map")
105}
106
107func TestQuadrant(t *testing.T) {
108 pal := Palette{"#000000", "#ff0000"}
109
110 // A solid 2x2 block of one index has no foreground at all.
111 solid := sprite(pal, "11", "11").Canvas(Quadrant)
112 uassert.Equal(t, ' ', solid.At(0, 0).R, "one colour means a blank cell on a solid background")
113 uassert.Equal(t, 0xff0000, solid.At(0, 0).BG, "and that colour is the background")
114 uassert.Equal(t, Default, solid.At(0, 0).FG, "with no foreground")
115
116 // Three background pixels and one foreground: the minority becomes the
117 // glyph. Bit 0 is top-left.
118 corner := sprite(pal, "10", "00").Canvas(Quadrant)
119 uassert.Equal(t, '▘', corner.At(0, 0).R, "top-left quadrant")
120 uassert.Equal(t, 0xff0000, corner.At(0, 0).FG, "the minority index is the foreground")
121
122 left := sprite(pal, "10", "10").Canvas(Quadrant)
123 uassert.Equal(t, '▌', left.At(0, 0).R, "left half")
124}
125
126func TestQuadrantPacksFourToOne(t *testing.T) {
127 p := sprite(Palette{"", "#fff"}, "1111", "1111", "1111", "1111")
128 c := p.Canvas(Quadrant)
129 uassert.Equal(t, 2, c.W, "half the columns")
130 uassert.Equal(t, 2, c.H, "half the rows")
131}
132
133func TestBraille(t *testing.T) {
134 p := sprite(Palette{"", "#ffffff"}, "10", "00", "00", "00")
135 c := p.Canvas(Braille)
136 uassert.Equal(t, 1, c.W, "2 columns pack into 1")
137 uassert.Equal(t, 1, c.H, "4 rows pack into 1")
138 uassert.Equal(t, rune(0x2801), c.At(0, 0).R, "top-left dot is bit 0")
139
140 // The fourth row took the high bits when it was added to the standard,
141 // which is the one thing about Braille that is not sequential.
142 low := sprite(Palette{"", "#ffffff"}, "00", "00", "00", "10").Canvas(Braille)
143 uassert.Equal(t, rune(0x2840), low.At(0, 0).R, "bottom-left dot is bit 6")
144
145 full := sprite(Palette{"", "#ffffff"}, "11", "11", "11", "11").Canvas(Braille)
146 uassert.Equal(t, rune(0x28ff), full.At(0, 0).R, "all eight dots")
147
148 empty := sprite(Palette{"", "#ffffff"}, "00").Canvas(Braille)
149 uassert.Equal(t, rune(0x2800), empty.At(0, 0).R, "blank braille, not a space")
150}
151
152func TestPixSVGRunLength(t *testing.T) {
153 pal := Palette{"", "#ff0000"}
154 p := sprite(pal, "0111", "0000")
155
156 doc := p.SVG(4).String()
157 // Index 0 is transparent, so it emits nothing; the three red pixels become
158 // ONE run in ONE path, not three rectangles.
159 uassert.Equal(t, 1, strings.Count(doc, "<path"), "one path per colour")
160 uassert.True(t, strings.Contains(doc, `d="M1 0h3v1h-3z"`), "one run, in pixel coordinates")
161 uassert.True(t, strings.Contains(doc, `fill="#ff0000"`), "run colour")
162 // Scale goes on the canvas, not into the path, so scaling costs no bytes.
163 uassert.True(t, strings.Contains(doc, `width="16" height="8"`), "canvas is scaled")
164 uassert.True(t, strings.Contains(doc, `viewBox="0 0 4 2"`), "viewBox stays in pixel units")
165}
166
167func TestPixSVGNormalisesColour(t *testing.T) {
168 // A short-form entry is expanded rather than passed through, which is what
169 // keeps an unparseable string a caller typed out of the document.
170 p := sprite(Palette{"#abc"}, "0")
171 uassert.True(t, strings.Contains(p.SVG(1).String(), `fill="#aabbcc"`), "short form expanded")
172
173 junk := sprite(Palette{`" onload="x`}, "0")
174 uassert.Equal(t, 0, strings.Count(junk.SVG(1).String(), "<path"), "an unparseable entry emits nothing")
175}
176
177func TestPaletteColorOutOfRange(t *testing.T) {
178 pal := Palette{"#000000"}
179 uassert.Equal(t, Default, pal.Color(7), "past the end is Default, not a panic")
180 uassert.Equal(t, Default, Palette{""}.Color(0), "an empty entry is transparent")
181}
182
183func distinctRunes(s string) int {
184 var seen []rune
185 for _, r := range s {
186 found := false
187 for _, o := range seen {
188 if o == r {
189 found = true
190 break
191 }
192 }
193 if !found {
194 seen = append(seen, r)
195 }
196 }
197 return len(seen)
198}