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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}