Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}