Add canvas, dither, sprites, clock and pixel elements
This commit is contained in:
11 files changed
+1274
-11
No files matched your search
+271
@@ -0,0 +1,271 @@
|
||||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// Canvas draws dots for one Draw call; coordinates are dots.
|
||||
type Canvas struct {
|
||||
p painter
|
||||
k Kit
|
||||
gx, gy int
|
||||
W, H int
|
||||
}
|
||||
|
||||
// painter is what Canvas needs of *ui.Painter.
|
||||
type painter interface {
|
||||
Fill(r ui.Rect, c ui.Color, radius float32)
|
||||
After(d time.Duration)
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
func (k Kit) Canvas(p *ui.Painter, r ui.Rect) Canvas {
|
||||
gx := int(math.Round(float64(r.X / k.dot)))
|
||||
gy := int(math.Round(float64(r.Y / k.dot)))
|
||||
return Canvas{p: p, k: k, gx: gx, gy: gy, W: int(r.W / k.dot), H: int(r.H / k.dot)}
|
||||
}
|
||||
|
||||
func (g Canvas) now() time.Time {
|
||||
if g.k.clock != nil {
|
||||
return g.k.clock()
|
||||
}
|
||||
return g.p.Now()
|
||||
}
|
||||
|
||||
func (g Canvas) Rect(x, y, w, h int, s Slot) {
|
||||
if w <= 0 || h <= 0 {
|
||||
return
|
||||
}
|
||||
d := g.k.dot
|
||||
g.p.Fill(ui.Rect{X: float32(g.gx+x) * d, Y: float32(g.gy+y) * d, W: float32(w) * d, H: float32(h) * d}, g.k.C(s), 0)
|
||||
}
|
||||
|
||||
func (g Canvas) Dot(x, y int, s Slot) { g.Rect(x, y, 1, 1, s) }
|
||||
func (g Canvas) HLine(x, y, w int, s Slot) { g.Rect(x, y, w, 1, s) }
|
||||
func (g Canvas) VLine(x, y, h int, s Slot) { g.Rect(x, y, 1, h, s) }
|
||||
|
||||
// Line draws a Bresenham line, both ends included.
|
||||
func (g Canvas) Line(x0, y0, x1, y1 int, s Slot) {
|
||||
dx, dy := abs(x1-x0), -abs(y1-y0)
|
||||
sx, sy := sign(x1-x0), sign(y1-y0)
|
||||
e := dx + dy
|
||||
var pts [][2]int
|
||||
for {
|
||||
pts = append(pts, [2]int{x0, y0})
|
||||
if x0 == x1 && y0 == y1 {
|
||||
break
|
||||
}
|
||||
e2 := 2 * e
|
||||
if e2 >= dy {
|
||||
e += dy
|
||||
x0 += sx
|
||||
}
|
||||
if e2 <= dx {
|
||||
e += dx
|
||||
y0 += sy
|
||||
}
|
||||
}
|
||||
g.points(pts, s)
|
||||
}
|
||||
|
||||
// points merges dots in a row or column into rects.
|
||||
func (g Canvas) points(pts [][2]int, s Slot) {
|
||||
if len(pts) == 0 {
|
||||
return
|
||||
}
|
||||
x0, y0, x1, y1 := pts[0][0], pts[0][1], pts[0][0], pts[0][1]
|
||||
for _, p := range pts[1:] {
|
||||
row := y0 == y1 && p[1] == y0 && (p[0] == x1+1 || p[0] == x0-1)
|
||||
col := x0 == x1 && p[0] == x0 && (p[1] == y1+1 || p[1] == y0-1)
|
||||
if row || col {
|
||||
x0, y0, x1, y1 = min(x0, p[0]), min(y0, p[1]), max(x1, p[0]), max(y1, p[1])
|
||||
continue
|
||||
}
|
||||
g.Rect(x0, y0, x1-x0+1, y1-y0+1, s)
|
||||
x0, y0, x1, y1 = p[0], p[1], p[0], p[1]
|
||||
}
|
||||
g.Rect(x0, y0, x1-x0+1, y1-y0+1, s)
|
||||
}
|
||||
|
||||
// Circle draws a midpoint circle outline of radius r.
|
||||
func (g Canvas) Circle(cx, cy, r int, s Slot) {
|
||||
rows := map[int][]int{}
|
||||
octants(r, func(x, y int) {
|
||||
for _, p := range [][2]int{{x, y}, {y, x}, {-x, y}, {-y, x}, {x, -y}, {y, -x}, {-x, -y}, {-y, -x}} {
|
||||
rows[cy+p[1]] = append(rows[cy+p[1]], cx+p[0])
|
||||
}
|
||||
})
|
||||
for y, xs := range rows {
|
||||
slices.Sort(xs)
|
||||
xs = slices.Compact(xs)
|
||||
start := 0
|
||||
for i := 1; i <= len(xs); i++ {
|
||||
if i == len(xs) || xs[i] != xs[i-1]+1 {
|
||||
g.HLine(xs[start], y, xs[i-1]-xs[start]+1, s)
|
||||
start = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disc fills a midpoint circle of radius r.
|
||||
func (g Canvas) Disc(cx, cy, r int, s Slot) {
|
||||
half := make([]int, r+1)
|
||||
octants(r, func(x, y int) {
|
||||
half[y] = max(half[y], x)
|
||||
half[x] = max(half[x], y)
|
||||
})
|
||||
for dy := -r; dy <= r; dy++ {
|
||||
h := half[abs(dy)]
|
||||
g.HLine(cx-h, cy+dy, 2*h+1, s)
|
||||
}
|
||||
}
|
||||
|
||||
func octants(r int, plot func(x, y int)) {
|
||||
if r < 0 {
|
||||
panic(fmt.Sprintf("pixel: negative radius %d", r))
|
||||
}
|
||||
x, y, e := r, 0, 1-r
|
||||
for x >= y {
|
||||
plot(x, y)
|
||||
y++
|
||||
if e < 0 {
|
||||
e += 2*y + 1
|
||||
} else {
|
||||
x--
|
||||
e += 2*(y-x) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bevel lights the top-left edge and darkens the bottom-right.
|
||||
func (g Canvas) Bevel(x, y, w, h int, light, dark Slot) {
|
||||
g.HLine(x, y, w-1, light)
|
||||
g.VLine(x, y+1, h-2, light)
|
||||
g.HLine(x+1, y+h-1, w-1, dark)
|
||||
g.VLine(x+w-1, y, h-1, dark)
|
||||
}
|
||||
|
||||
type FrameStyle uint8
|
||||
|
||||
const (
|
||||
Raised FrameStyle = iota
|
||||
Sunken
|
||||
Flat
|
||||
Double
|
||||
Window
|
||||
Notched
|
||||
)
|
||||
|
||||
// Frame draws an outlined, filled box in a style.
|
||||
func (g Canvas) Frame(x, y, w, h int, style FrameStyle, fill Slot) {
|
||||
if w < 3 || h < 3 {
|
||||
panic(fmt.Sprintf("pixel: frame %d×%d smaller than 3×3", w, h))
|
||||
}
|
||||
notch := style == Window || style == Notched
|
||||
g.outline(x, y, w, h, Edge, notch)
|
||||
g.Rect(x+1, y+1, w-2, h-2, fill)
|
||||
switch style {
|
||||
case Raised:
|
||||
g.Bevel(x+1, y+1, w-2, h-2, Lit, Shade)
|
||||
case Sunken:
|
||||
g.Bevel(x+1, y+1, w-2, h-2, Shade, Lit)
|
||||
case Double:
|
||||
if w >= 5 && h >= 5 {
|
||||
g.outline(x+2, y+2, w-4, h-4, Edge, false)
|
||||
}
|
||||
case Window:
|
||||
g.outline(x+1, y+1, w-2, h-2, Lit, false)
|
||||
if w >= 5 && h >= 5 {
|
||||
g.outline(x+2, y+2, w-4, h-4, Shade, false)
|
||||
}
|
||||
case Flat, Notched:
|
||||
default:
|
||||
panic(fmt.Sprintf("pixel: unknown frame style %d", style))
|
||||
}
|
||||
}
|
||||
|
||||
func (g Canvas) outline(x, y, w, h int, s Slot, notch bool) {
|
||||
n := 0
|
||||
if notch {
|
||||
n = 1
|
||||
}
|
||||
g.HLine(x+n, y, w-2*n, s)
|
||||
g.HLine(x+n, y+h-1, w-2*n, s)
|
||||
g.VLine(x, y+1, h-2, s)
|
||||
g.VLine(x+w-1, y+1, h-2, s)
|
||||
}
|
||||
|
||||
// Text draws one line with its top-left at x, y.
|
||||
func (g Canvas) Text(x, y int, s string, ink Slot, scale int) {
|
||||
checkScale(scale)
|
||||
for _, r := range s {
|
||||
gl := glyphOf(r)
|
||||
top := y + gl.top()*scale
|
||||
for row := range gl.h {
|
||||
g.bits(x+gl.x*scale, top+row*scale, gl.w, scale, ink, func(col int) bool { return gl.on(col, row) })
|
||||
}
|
||||
x += gl.adv * scale
|
||||
}
|
||||
}
|
||||
|
||||
// Micro draws s in the 3×5 micro font.
|
||||
func (g Canvas) Micro(x, y int, s string, ink Slot, scale int) {
|
||||
checkScale(scale)
|
||||
for _, r := range s {
|
||||
gl := microGlyph(r)
|
||||
for row, bits := range gl {
|
||||
g.bits(x, y+row*scale, 3, scale, ink, func(col int) bool { return bits&(4>>col) != 0 })
|
||||
}
|
||||
x += MicroAdvance * scale
|
||||
}
|
||||
}
|
||||
|
||||
// bits draws one row's set dots as merged runs.
|
||||
func (g Canvas) bits(x, y, w, scale int, s Slot, on func(int) bool) {
|
||||
runs(w, on, func(i, j int) { g.Rect(x+i*scale, y, (j-i)*scale, scale, s) })
|
||||
}
|
||||
|
||||
// runs emits each run [i, j) of set dots.
|
||||
func runs(w int, on func(int) bool, emit func(i, j int)) {
|
||||
for i := 0; i < w; {
|
||||
if !on(i) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
j := i
|
||||
for j < w && on(j) {
|
||||
j++
|
||||
}
|
||||
emit(i, j)
|
||||
i = j
|
||||
}
|
||||
}
|
||||
|
||||
func checkScale(scale int) {
|
||||
if scale < 1 || scale > 4 {
|
||||
panic(fmt.Sprintf("pixel: scale %d outside 1–4", scale))
|
||||
}
|
||||
}
|
||||
|
||||
func abs(v int) int {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func sign(v int) int {
|
||||
switch {
|
||||
case v > 0:
|
||||
return 1
|
||||
case v < 0:
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package pixel_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestRectAndLines(t *testing.T) {
|
||||
d := canvas(t, 8, 6, func(g pixel.Canvas) {
|
||||
g.Rect(1, 1, 3, 2, pixel.Accent)
|
||||
g.HLine(0, 4, 5, pixel.Good)
|
||||
g.VLine(7, 0, 6, pixel.Bad)
|
||||
g.Dot(6, 5, pixel.Magic)
|
||||
g.Rect(5, 1, 0, 3, pixel.Ink)
|
||||
})
|
||||
if d.count(1, 1, 3, 2, pixel.Accent) != 6 || d.at(0, 1) != pixel.Back || d.at(4, 2) != pixel.Back {
|
||||
t.Fatal("Rect covers the wrong dots")
|
||||
}
|
||||
if d.count(0, 4, 5, 1, pixel.Good) != 5 || d.at(5, 4) != pixel.Back {
|
||||
t.Fatal("HLine covers the wrong dots")
|
||||
}
|
||||
if d.count(7, 0, 1, 6, pixel.Bad) != 6 || d.at(6, 5) != pixel.Magic {
|
||||
t.Fatal("VLine or Dot wrong")
|
||||
}
|
||||
if d.count(5, 1, 1, 3, pixel.Ink) != 0 {
|
||||
t.Fatal("zero-width Rect drew")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineBresenham(t *testing.T) {
|
||||
cases := [][4]int{{0, 0, 9, 3}, {9, 0, 0, 3}, {2, 0, 4, 9}, {0, 9, 6, 0}, {3, 3, 3, 3}, {0, 5, 9, 5}}
|
||||
for _, c := range cases {
|
||||
d := canvas(t, 10, 10, func(g pixel.Canvas) { g.Line(c[0], c[1], c[2], c[3], pixel.Ink) })
|
||||
if d.at(c[0], c[1]) != pixel.Ink || d.at(c[2], c[3]) != pixel.Ink {
|
||||
t.Fatalf("line %v misses an end", c)
|
||||
}
|
||||
dx, dy := c[2]-c[0], c[3]-c[1]
|
||||
major := max(dx, -dx, dy, -dy)
|
||||
if got := d.count(0, 0, 10, 10, pixel.Ink); got != major+1 {
|
||||
t.Fatalf("line %v has %d dots, want %d", c, got, major+1)
|
||||
}
|
||||
xMajor := max(dx, -dx) >= max(dy, -dy)
|
||||
for i := range 10 {
|
||||
var n int
|
||||
if xMajor {
|
||||
n = d.count(i, 0, 1, 10, pixel.Ink)
|
||||
} else {
|
||||
n = d.count(0, i, 10, 1, pixel.Ink)
|
||||
}
|
||||
inside := i >= min(c[0], c[2]) && i <= max(c[0], c[2])
|
||||
if !xMajor {
|
||||
inside = i >= min(c[1], c[3]) && i <= max(c[1], c[3])
|
||||
}
|
||||
if inside != (n == 1) || n > 1 {
|
||||
t.Fatalf("line %v: %d dots across major step %d", c, n, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineTieBreak(t *testing.T) {
|
||||
d := canvas(t, 3, 2, func(g pixel.Canvas) { g.Line(0, 0, 2, 1, pixel.Ink) })
|
||||
for _, p := range [][2]int{{0, 0}, {1, 1}, {2, 1}} {
|
||||
if d.at(p[0], p[1]) != pixel.Ink {
|
||||
t.Fatalf("Line(0,0,2,1) misses %v", p)
|
||||
}
|
||||
}
|
||||
if d.at(1, 0) != pixel.Back {
|
||||
t.Fatal("Line(0,0,2,1) took the wrong tie")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircleAndDisc(t *testing.T) {
|
||||
const cx, cy, r = 7, 7, 5
|
||||
disc := canvas(t, 15, 15, func(g pixel.Canvas) { g.Disc(cx, cy, r, pixel.Info) })
|
||||
ring := canvas(t, 15, 15, func(g pixel.Canvas) { g.Circle(cx, cy, r, pixel.Ink) })
|
||||
for _, p := range [][2]int{{cx + r, cy}, {cx - r, cy}, {cx, cy + r}, {cx, cy - r}} {
|
||||
if disc.at(p[0], p[1]) != pixel.Info || ring.at(p[0], p[1]) != pixel.Ink {
|
||||
t.Fatalf("radius point %v missing", p)
|
||||
}
|
||||
}
|
||||
if disc.at(cx, cy) != pixel.Info || ring.at(cx, cy) != pixel.Back || disc.at(cx+r, cy+r) != pixel.Back {
|
||||
t.Fatal("disc must be solid, circle hollow, corners empty")
|
||||
}
|
||||
for y := range 15 {
|
||||
for x := range 15 {
|
||||
if disc.at(x, y) != disc.at(2*cx-x, y) || disc.at(x, y) != disc.at(x, 2*cy-y) || disc.at(x, y) != disc.at(y, x) {
|
||||
t.Fatalf("disc not symmetric at %d,%d", x, y)
|
||||
}
|
||||
if ring.at(x, y) == pixel.Ink && disc.at(x, y) != pixel.Info {
|
||||
t.Fatalf("circle dot %d,%d outside the disc", x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircleExact(t *testing.T) {
|
||||
ring := canvas(t, 7, 7, func(g pixel.Canvas) { g.Circle(3, 3, 3, pixel.Ink) })
|
||||
disc := canvas(t, 7, 7, func(g pixel.Canvas) { g.Disc(3, 3, 3, pixel.Ink) })
|
||||
wantRing := []string{"..###..", ".#...#.", "#.....#", "#.....#", "#.....#", ".#...#.", "..###.."}
|
||||
wantDisc := []string{"..###..", ".#####.", "#######", "#######", "#######", ".#####.", "..###.."}
|
||||
for y := range 7 {
|
||||
for x := range 7 {
|
||||
if (ring.at(x, y) == pixel.Ink) != (wantRing[y][x] == '#') || (disc.at(x, y) == pixel.Ink) != (wantDisc[y][x] == '#') {
|
||||
t.Fatalf("radius 3 dot %d,%d differs from the midpoint shape", x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCircleRadiusSix(t *testing.T) {
|
||||
octant := [][2]int{{6, 0}, {6, 1}, {6, 2}, {5, 3}, {4, 4}}
|
||||
want := map[[2]int]bool{}
|
||||
for _, p := range octant {
|
||||
for _, q := range [][2]int{{p[0], p[1]}, {p[1], p[0]}} {
|
||||
for _, s := range [][2]int{{1, 1}, {-1, 1}, {1, -1}, {-1, -1}} {
|
||||
want[[2]int{6 + s[0]*q[0], 6 + s[1]*q[1]}] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
d := canvas(t, 13, 13, func(g pixel.Canvas) { g.Circle(6, 6, 6, pixel.Ink) })
|
||||
for y := range 13 {
|
||||
for x := range 13 {
|
||||
if (d.at(x, y) == pixel.Ink) != want[[2]int{x, y}] {
|
||||
t.Fatalf("radius 6 dot %d,%d differs from the midpoint shape", x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBayerMatrix(t *testing.T) {
|
||||
want := [4][4]int{{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}}
|
||||
first := [4][4]int{}
|
||||
for level := 16; level >= 1; level-- {
|
||||
d := canvas(t, 4, 4, func(g pixel.Canvas) { g.Veil(0, 0, 4, 4, pixel.Ink, level) })
|
||||
for y := range 4 {
|
||||
for x := range 4 {
|
||||
if d.at(x, y) == pixel.Ink {
|
||||
first[y][x] = level - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if first != want {
|
||||
t.Fatalf("Bayer thresholds %v, want %v", first, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameStyles(t *testing.T) {
|
||||
const w, h = 8, 7
|
||||
want := map[pixel.FrameStyle][][3]int{
|
||||
pixel.Flat: {{0, 0, int(pixel.Edge)}, {1, 1, int(pixel.Face)}, {w - 1, h - 1, int(pixel.Edge)}},
|
||||
pixel.Raised: {{0, 0, int(pixel.Edge)}, {1, 1, int(pixel.Lit)}, {w - 2, h - 2, int(pixel.Shade)}, {2, 2, int(pixel.Face)}},
|
||||
pixel.Sunken: {{1, 1, int(pixel.Shade)}, {w - 2, h - 2, int(pixel.Lit)}, {2, 2, int(pixel.Face)}},
|
||||
pixel.Double: {{0, 0, int(pixel.Edge)}, {1, 1, int(pixel.Face)}, {2, 2, int(pixel.Edge)}, {3, 3, int(pixel.Face)}},
|
||||
pixel.Window: {{0, 0, int(pixel.Back)}, {1, 0, int(pixel.Edge)}, {1, 1, int(pixel.Lit)}, {2, 2, int(pixel.Shade)}, {3, 3, int(pixel.Face)}},
|
||||
pixel.Notched: {{0, 0, int(pixel.Back)}, {1, 0, int(pixel.Edge)}, {0, 1, int(pixel.Edge)}, {1, 1, int(pixel.Face)}},
|
||||
}
|
||||
for style, checks := range want {
|
||||
d := canvas(t, w, h, func(g pixel.Canvas) { g.Frame(0, 0, w, h, style, pixel.Face) })
|
||||
for _, ck := range checks {
|
||||
if got := d.at(ck[0], ck[1]); got != pixel.Slot(ck[2]) {
|
||||
t.Errorf("style %d dot %d,%d = slot %d, want %d", style, ck[0], ck[1], got, ck[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
mustPanic(t, "2×2 frame", func() {
|
||||
canvas(t, 4, 4, func(g pixel.Canvas) { g.Frame(0, 0, 2, 2, pixel.Flat, pixel.Face) })
|
||||
})
|
||||
}
|
||||
|
||||
func TestDitherLevels(t *testing.T) {
|
||||
for level := 0; level <= 16; level++ {
|
||||
d := canvas(t, 8, 8, func(g pixel.Canvas) { g.Dither(0, 0, 8, 8, pixel.Face, pixel.Ink, level) })
|
||||
for _, b := range [][2]int{{0, 0}, {4, 0}, {0, 4}, {4, 4}} {
|
||||
if n := d.count(b[0], b[1], 4, 4, pixel.Ink); n != level {
|
||||
t.Fatalf("level %d: 4×4 block at %v has %d ink dots", level, b, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
d := canvas(t, 6, 6, func(g pixel.Canvas) { g.Checker(0, 0, 6, 6, pixel.Ink) })
|
||||
for y := range 6 {
|
||||
for x := range 6 {
|
||||
if want := (x+y)%2 == 0; (d.at(x, y) == pixel.Ink) != want {
|
||||
t.Fatalf("checker dot %d,%d", x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDitherAnchorsToWindow(t *testing.T) {
|
||||
d := canvas(t, 8, 4, func(g pixel.Canvas) {
|
||||
g.Rect(0, 0, 8, 4, pixel.Accent)
|
||||
g.Veil(1, 0, 3, 4, pixel.Ink, 8)
|
||||
g.Veil(4, 1, 4, 3, pixel.Ink, 8)
|
||||
})
|
||||
for y := range 4 {
|
||||
for x := 1; x < 8; x++ {
|
||||
if x >= 4 && y == 0 {
|
||||
continue
|
||||
}
|
||||
want := pixel.Accent
|
||||
if (x+y)%2 == 0 {
|
||||
want = pixel.Ink
|
||||
}
|
||||
if got := d.at(x, y); got != want {
|
||||
t.Fatalf("veil dot %d,%d = %d, want %d", x, y, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
mustPanic(t, "level 17", func() {
|
||||
canvas(t, 2, 2, func(g pixel.Canvas) { g.Veil(0, 0, 1, 1, pixel.Ink, 17) })
|
||||
})
|
||||
}
|
||||
|
||||
func TestTextAndMicroDraw(t *testing.T) {
|
||||
a := []string{"...#...", "...#...", "..#.#..", "..#.#..", ".#...#.", ".#####.", ".#...#.", "#.....#", "#.....#"}
|
||||
for _, scale := range []int{1, 2} {
|
||||
d := canvas(t, 8*scale, 16*scale, func(g pixel.Canvas) { g.Text(0, 0, "A", pixel.Ink, scale) })
|
||||
for row, line := range a {
|
||||
for col, ch := range line {
|
||||
if got := d.at(col*scale, (4+row)*scale) == pixel.Ink; got != (ch == '#') {
|
||||
t.Fatalf("×%d 'A' dot %d,%d", scale, col, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.count(0, 0, 8*scale, 4*scale, pixel.Ink) != 0 {
|
||||
t.Fatalf("×%d 'A' drew above its top", scale)
|
||||
}
|
||||
}
|
||||
one := []string{".#.", "##.", ".#.", ".#.", "###"}
|
||||
d := canvas(t, 8, 5, func(g pixel.Canvas) { g.Micro(0, 0, "11", pixel.Good, 1) })
|
||||
for row, line := range one {
|
||||
for col, ch := range line {
|
||||
if (d.at(col, row) == pixel.Good) != (ch == '#') || (d.at(col+4, row) == pixel.Good) != (ch == '#') {
|
||||
t.Fatalf("micro '1' dot %d,%d", col, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
var due atomic.Int64
|
||||
|
||||
// Due is the earliest repaint asked for since ResetDue.
|
||||
func Due() time.Time {
|
||||
n := due.Load()
|
||||
if n == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(0, n)
|
||||
}
|
||||
|
||||
func ResetDue() { due.Store(0) }
|
||||
|
||||
func ask(at int64) {
|
||||
for {
|
||||
cur := due.Load()
|
||||
if cur != 0 && cur <= at || due.CompareAndSwap(cur, at) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step is the frame at fps, frozen under Reduce Motion.
|
||||
func (g Canvas) Step(fps int) int {
|
||||
checkFPS(fps)
|
||||
if g.k.reduce {
|
||||
return 0
|
||||
}
|
||||
return g.Busy(fps)
|
||||
}
|
||||
|
||||
// Busy is the frame at fps, ignoring Reduce Motion.
|
||||
func (g Canvas) Busy(fps int) int {
|
||||
n, d := frameAt(g.now(), fps)
|
||||
g.p.After(d)
|
||||
ask(g.p.Now().Add(d).UnixNano())
|
||||
return n
|
||||
}
|
||||
|
||||
// Tick is the frame at fps, rebuilding the view.
|
||||
func (k Kit) Tick(c *ui.Context, fps int) int {
|
||||
n, d := frameAt(k.now, fps)
|
||||
c.After(d)
|
||||
ask(c.Now().Add(d).UnixNano())
|
||||
return n
|
||||
}
|
||||
|
||||
// frameAt is the frame at t and the next wait.
|
||||
func frameAt(t time.Time, fps int) (int, time.Duration) {
|
||||
checkFPS(fps)
|
||||
period := int64(time.Second) / int64(fps)
|
||||
ns := t.UnixNano()
|
||||
n := ns / period
|
||||
return int(n), time.Duration((n+1)*period - ns)
|
||||
}
|
||||
|
||||
func checkFPS(fps int) {
|
||||
if fps < 1 || fps > 60 {
|
||||
panic(fmt.Sprintf("pixel: fps %d outside 1–60", fps))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package pixel
|
||||
|
||||
import "fmt"
|
||||
|
||||
var bayer = [4][4]int{{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}}
|
||||
|
||||
// Dither fills b below the level threshold, a elsewhere.
|
||||
func (g Canvas) Dither(x, y, w, h int, a, b Slot, level int) {
|
||||
checkLevel(level)
|
||||
if level <= 8 {
|
||||
g.Rect(x, y, w, h, a)
|
||||
g.cells(x, y, w, h, b, func(t int) bool { return t < level })
|
||||
return
|
||||
}
|
||||
g.Rect(x, y, w, h, b)
|
||||
g.cells(x, y, w, h, a, func(t int) bool { return t >= level })
|
||||
}
|
||||
|
||||
// Veil dithers s over what is already there.
|
||||
func (g Canvas) Veil(x, y, w, h int, s Slot, level int) {
|
||||
checkLevel(level)
|
||||
g.cells(x, y, w, h, s, func(t int) bool { return t < level })
|
||||
}
|
||||
|
||||
// Checker is a half Veil: every other dot.
|
||||
func (g Canvas) Checker(x, y, w, h int, s Slot) { g.Veil(x, y, w, h, s, 8) }
|
||||
|
||||
func (g Canvas) cells(x, y, w, h int, s Slot, on func(t int) bool) {
|
||||
for j := range h {
|
||||
row := bayer[(g.gy+y+j)&3]
|
||||
g.bits(x, y+j, w, 1, s, func(i int) bool { return on(row[(g.gx+x+i)&3]) })
|
||||
}
|
||||
}
|
||||
|
||||
func checkLevel(level int) {
|
||||
if level < 0 || level > 16 {
|
||||
panic(fmt.Sprintf("pixel: dither level %d outside 0–16", level))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
type TextOptions struct {
|
||||
Slot Slot
|
||||
// Scale is 1–4; zero means 1.
|
||||
Scale int
|
||||
// Wrap breaks lines at the width the layout gives.
|
||||
Wrap bool
|
||||
MaxLines int
|
||||
}
|
||||
|
||||
// Text is a pixel-font label that screen readers read.
|
||||
func Text(c *ui.Context, s string, o TextOptions) *ui.Element {
|
||||
k := K(c)
|
||||
scale := scaleOr1(o.Scale)
|
||||
e := ui.Box(c).Label(s).Role(ui.RoleText)
|
||||
width, check := 0, func(ui.Rect) {}
|
||||
if o.Wrap {
|
||||
width, check = WrapWidth(c, e)
|
||||
}
|
||||
lines := textLines(s, width/scale, o.MaxLines)
|
||||
if o.Wrap {
|
||||
e.Height(k.D(len(lines) * LineHeight * scale))
|
||||
} else {
|
||||
w := 0
|
||||
for _, l := range lines {
|
||||
w = max(w, Measure(l))
|
||||
}
|
||||
e.Size(k.D(w*scale), k.D(len(lines)*LineHeight*scale))
|
||||
}
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
check(r)
|
||||
g := k.Canvas(p, r)
|
||||
for i, l := range lines {
|
||||
g.Text(0, i*LineHeight*scale, l, o.Slot, scale)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// textLines wraps s, cutting to maxLines with an ellipsis.
|
||||
func textLines(s string, width, maxLines int) []string {
|
||||
if maxLines < 0 {
|
||||
panic(fmt.Sprintf("pixel: MaxLines %d", maxLines))
|
||||
}
|
||||
lines := Lines(s, width)
|
||||
if maxLines == 0 || len(lines) <= maxLines {
|
||||
return lines
|
||||
}
|
||||
lines = lines[:maxLines]
|
||||
last := []rune(strings.TrimRight(lines[maxLines-1], " "))
|
||||
for width > 0 && len(last) > 0 && Measure(string(last)+"…") > width {
|
||||
last = last[:len(last)-1]
|
||||
}
|
||||
lines[maxLines-1] = string(last) + "…"
|
||||
return lines
|
||||
}
|
||||
|
||||
type wrapKey struct{}
|
||||
|
||||
// WrapWidth is e's last painted width; Draw must call check.
|
||||
func WrapWidth(c *ui.Context, e *ui.Element) (int, func(r ui.Rect)) {
|
||||
dot := K(c).dot
|
||||
w := ui.Local(e, wrapKey{}, func() int { return 0 })
|
||||
used := *w
|
||||
return used, func(r ui.Rect) {
|
||||
if now := int(r.W / dot); now != *w {
|
||||
*w = now
|
||||
c.Invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type IconOptions struct {
|
||||
Slot Slot
|
||||
Scale int
|
||||
// Label names the icon; empty hides it from assistive technology.
|
||||
Label string
|
||||
}
|
||||
|
||||
func Icon(c *ui.Context, sp *Sprite, o IconOptions) *ui.Element {
|
||||
k := K(c)
|
||||
scale := scaleOr1(o.Scale)
|
||||
e := ui.Box(c).Size(k.D(sp.W*scale), k.D(sp.H*scale))
|
||||
if o.Label != "" {
|
||||
e.Label(o.Label).Role(ui.RoleImage)
|
||||
} else {
|
||||
e.Role(ui.RoleNone)
|
||||
}
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
k.Canvas(p, r).Sprite(0, 0, sp, 0, o.Slot, scale)
|
||||
})
|
||||
}
|
||||
|
||||
func scaleOr1(s int) int {
|
||||
if s == 0 {
|
||||
return 1
|
||||
}
|
||||
checkScale(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// State is how a control looks this frame.
|
||||
type State struct{ Hover, Press, Focus, Disabled bool }
|
||||
|
||||
// Shell wraps base in an enabled box that paints.
|
||||
func Shell(c *ui.Context, base func() *ui.Element) (shell, b *ui.Element, st State) {
|
||||
shell = ui.Box(c)
|
||||
shell.Children(func() { b = base().Fill().FocusRing(false) })
|
||||
st = State{Hover: b.Hovered(), Press: b.Pressed(), Focus: b.FocusVisible(), Disabled: b.IsDisabled()}
|
||||
return shell, b, st
|
||||
}
|
||||
|
||||
var hand = MustSprite(`
|
||||
.eee......
|
||||
elllleeee.
|
||||
elllllllle
|
||||
elllleeee.
|
||||
elllle....
|
||||
.eeee.....
|
||||
`)
|
||||
|
||||
// FocusMark rings a box with dots and points a hand.
|
||||
func (g Canvas) FocusMark(x, y, w, h int) {
|
||||
rx, ry, rw, rh := x-2, y-2, w+4, h+4
|
||||
for i := range rw {
|
||||
g.ant(rx+i, ry)
|
||||
g.ant(rx+i, ry+rh-1)
|
||||
}
|
||||
for j := 1; j < rh-1; j++ {
|
||||
g.ant(rx, ry+j)
|
||||
g.ant(rx+rw-1, ry+j)
|
||||
}
|
||||
g.Sprite(rx-hand.W-1+g.Step(4)%2, y+(h-hand.H)/2, hand, 0, Edge, 1)
|
||||
}
|
||||
|
||||
func (g Canvas) ant(x, y int) {
|
||||
if (g.gx+x+g.gy+y)&1 == 0 {
|
||||
g.Dot(x, y, Accent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package pixel_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestSpriteLettersMatchSlots(t *testing.T) {
|
||||
sp := pixel.MustSprite("kbflsenwaAigyrcm")
|
||||
d := canvas(t, 16, 1, func(g pixel.Canvas) { g.Sprite(0, 0, sp, 0, pixel.Ink, 1) })
|
||||
for i := range 16 {
|
||||
if got := d.at(i, 0); got != pixel.Slot(i) {
|
||||
t.Fatalf("letter %d drew slot %d", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpriteFramesMirrorScale(t *testing.T) {
|
||||
sp := pixel.MustSprite(`
|
||||
#a.
|
||||
..g
|
||||
|
||||
.r.
|
||||
.r.
|
||||
`)
|
||||
if sp.W != 3 || sp.H != 2 || sp.Frames() != 2 {
|
||||
t.Fatalf("sprite %d×%d with %d frames", sp.W, sp.H, sp.Frames())
|
||||
}
|
||||
d := canvas(t, 6, 4, func(g pixel.Canvas) {
|
||||
g.Sprite(0, 0, sp, 0, pixel.Magic, 1)
|
||||
g.Sprite(3, 0, sp, 3, pixel.Ink, 1)
|
||||
g.Sprite(0, 2, sp.Mirror(), 0, pixel.Magic, 1)
|
||||
})
|
||||
checks := map[[2]int]pixel.Slot{
|
||||
{0, 0}: pixel.Magic, {1, 0}: pixel.Accent, {2, 0}: pixel.Back, {2, 1}: pixel.Good,
|
||||
{4, 0}: pixel.Bad, {4, 1}: pixel.Bad, {3, 0}: pixel.Back,
|
||||
{2, 2}: pixel.Magic, {1, 2}: pixel.Accent, {0, 3}: pixel.Good,
|
||||
}
|
||||
for p, want := range checks {
|
||||
if got := d.at(p[0], p[1]); got != want {
|
||||
t.Fatalf("dot %v = %d, want %d", p, got, want)
|
||||
}
|
||||
}
|
||||
big := canvas(t, 6, 4, func(g pixel.Canvas) { g.Sprite(0, 0, sp, 0, pixel.Magic, 2) })
|
||||
if big.count(0, 0, 2, 2, pixel.Magic) != 4 || big.count(2, 0, 2, 2, pixel.Accent) != 4 {
|
||||
t.Fatal("scale 2 must fill 2×2 dots per art pixel")
|
||||
}
|
||||
for name, art := range map[string]string{"ragged": "ab\nabc", "letter": "az", "empty": " ", "frames": "aa\n\naa\naa"} {
|
||||
mustPanic(t, name, func() { pixel.MustSprite(art) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestClock(t *testing.T) {
|
||||
at := time.Unix(1_700_000_001, 250_000_000)
|
||||
var step, busy, tick int
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
|
||||
k := pixel.K(c)
|
||||
tick = k.Tick(c, 8)
|
||||
ui.Box(c).Size(4, 4).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
step, busy = g.Step(4), g.Busy(4)
|
||||
})
|
||||
}, 10, 10)
|
||||
if want := int(at.UnixNano() / int64(time.Second/4)); step != want || busy != want {
|
||||
t.Fatalf("Step %d Busy %d, want %d", step, busy, want)
|
||||
}
|
||||
if want := int(at.UnixNano() / int64(time.Second/8)); tick != want {
|
||||
t.Fatalf("Tick %d, want %d", tick, want)
|
||||
}
|
||||
pixel.ResetDue()
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
if step != 0 || busy == 0 {
|
||||
t.Fatalf("under Reduce Motion: Step %d (want 0), Busy %d", step, busy)
|
||||
}
|
||||
if due := time.Until(pixel.Due()); due <= 0 || due > 125*time.Millisecond {
|
||||
t.Fatalf("Due in %v, want the 8 fps wait on the real clock", due)
|
||||
}
|
||||
pixel.ResetDue()
|
||||
if !pixel.Due().IsZero() {
|
||||
t.Fatal("ResetDue left a deadline")
|
||||
}
|
||||
mustPanic(t, "fps 0", func() {
|
||||
ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
|
||||
pixel.K(c).Tick(c, 0)
|
||||
}, 10, 10)
|
||||
})
|
||||
mustPanic(t, "Step 0 under Reduce Motion", func() {
|
||||
on := false
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
|
||||
k := pixel.K(c)
|
||||
ui.Box(c).Size(4, 4).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
if on {
|
||||
k.Canvas(p, r).Step(0)
|
||||
}
|
||||
})
|
||||
}, 10, 10)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
on = true
|
||||
tt.Frame()
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrapFollowsSameFrameResize(t *testing.T) {
|
||||
const s = "像素风格组件库,每个点都经过认真摆放,在限制中创造丰富。"
|
||||
width := float32(120)
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Dot: 2})
|
||||
ui.Column(c).Width(width).Children(func() {
|
||||
pixel.Text(c, s, pixel.TextOptions{Wrap: true})
|
||||
})
|
||||
}, 400, 800)
|
||||
for _, w := range []float32{60, 200, 120} {
|
||||
width = w
|
||||
tt.Frame()
|
||||
r, _ := tt.Find(s)
|
||||
if want := float32(len(pixel.Lines(s, int(w/2))) * 16 * 2); r.H != want {
|
||||
t.Fatalf("width %v: text height %v, want %v", w, r.H, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextElement(t *testing.T) {
|
||||
var width float32
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Dot: 2})
|
||||
pixel.Text(c, "HELLO", pixel.TextOptions{Scale: 2})
|
||||
ui.Column(c).Width(120).Children(func() {
|
||||
pixel.Text(c, "像素风格组件库,每个点都经过认真摆放。", pixel.TextOptions{Wrap: true})
|
||||
})
|
||||
pixel.Text(c, "one\ntwo\nthree", pixel.TextOptions{MaxLines: 2})
|
||||
width = float32(pixel.Measure("HELLO") * 2 * 2)
|
||||
}, 400, 400)
|
||||
r, ok := tt.Find("HELLO")
|
||||
if !ok || r.W != width || r.H != 2*16*2 {
|
||||
t.Fatalf("HELLO box %v (found %v), want %v×64", r, ok, width)
|
||||
}
|
||||
r, _ = tt.Find("像素风格组件库,每个点都经过认真摆放。")
|
||||
if lines := len(pixel.Lines("像素风格组件库,每个点都经过认真摆放。", 60)); r.H != float32(lines*16*2) || lines < 3 {
|
||||
t.Fatalf("wrapped box %v, want %d lines", r, lines)
|
||||
}
|
||||
if r, _ = tt.Find("one\ntwo\nthree"); r.H != 2*16*2 {
|
||||
t.Fatalf("MaxLines box height %v, want 64", r.H)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconElement(t *testing.T) {
|
||||
sp := pixel.MustSprite("aa\naa\naa")
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Dot: 2})
|
||||
pixel.Icon(c, sp, pixel.IconOptions{Scale: 3, Label: "block"})
|
||||
pixel.Icon(c, sp, pixel.IconOptions{})
|
||||
}, 100, 100)
|
||||
r, ok := tt.Find("block")
|
||||
if !ok || r.W != 2*3*2 || r.H != 3*3*2 {
|
||||
t.Fatalf("icon box %v found %v, want 12×18", r, ok)
|
||||
}
|
||||
if len(tt.Texts()) != 1 {
|
||||
t.Fatalf("unlabeled icon is visible to assistive technology: %q", tt.Texts())
|
||||
}
|
||||
}
|
||||
|
||||
func shellView(disabled *bool, clicks *int, st *pixel.State) func(c *ui.Context) {
|
||||
at := time.Unix(1_700_000_001, 250_000_000)
|
||||
return func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SummerDay, Dot: 2, Now: func() time.Time { return at }})
|
||||
k := pixel.K(c)
|
||||
ui.Box(c).Size(k.D(16), k.D(4))
|
||||
ui.Row(c).Children(func() {
|
||||
ui.Box(c).Size(k.D(16), k.D(8))
|
||||
shell, base, s := pixel.Shell(c, func() *ui.Element {
|
||||
return ui.ButtonBase(c).Label("go").Disabled(*disabled)
|
||||
})
|
||||
*st = s
|
||||
if base.Clicked() {
|
||||
*clicks++
|
||||
}
|
||||
shell.Size(k.D(12), k.D(8)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Rect(0, 0, 12, 8, pixel.Accent)
|
||||
if s.Focus {
|
||||
g.FocusMark(0, 0, 12, 8)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellKeepsLookAndBehavior(t *testing.T) {
|
||||
disabled, clicks, st := false, 0, pixel.State{}
|
||||
tt := ui.NewTester(shellView(&disabled, &clicks, &st), 100, 100)
|
||||
tt.ClickAt(40, 16)
|
||||
if clicks != 1 {
|
||||
t.Fatalf("enabled shell: %d clicks", clicks)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Frame()
|
||||
if !st.Focus {
|
||||
t.Fatal("Tab did not give the base visible focus")
|
||||
}
|
||||
d := dots{t: t, img: tt.Image(), pal: pixel.SummerDay}
|
||||
if d.at(14, 2) != pixel.Accent || d.at(15, 2) != pixel.Back || d.at(29, 13) != pixel.Accent {
|
||||
t.Fatal("focus ring dots missing around the shell")
|
||||
}
|
||||
if d.count(3, 5, 12, 6, pixel.Edge) == 0 || d.count(3, 5, 12, 6, pixel.Lit) == 0 {
|
||||
t.Fatal("hand cursor missing left of the shell")
|
||||
}
|
||||
disabled = true
|
||||
tt.Frame()
|
||||
tt.ClickAt(40, 16)
|
||||
if clicks != 1 || !st.Disabled || st.Focus {
|
||||
t.Fatalf("disabled: clicks %d, state %+v", clicks, st)
|
||||
}
|
||||
d = dots{t: t, img: tt.Image(), pal: pixel.SummerDay}
|
||||
if d.count(16, 4, 12, 8, pixel.Accent) != 96 {
|
||||
t.Fatal("disabled base dimmed the shell's look")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVeilAnchorsAcrossElements(t *testing.T) {
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SummerDay, Dot: 2})
|
||||
k := pixel.K(c)
|
||||
ui.Box(c).Size(k.D(1), k.D(1))
|
||||
ui.Row(c).Children(func() {
|
||||
ui.Box(c).Size(k.D(3), k.D(2))
|
||||
ui.Box(c).Size(k.D(4), k.D(2)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
k.Canvas(p, r).Checker(0, 0, 4, 2, pixel.Ink)
|
||||
})
|
||||
})
|
||||
}, 14, 6)
|
||||
d := dots{t: t, img: tt.Image(), pal: pixel.SummerDay}
|
||||
if d.at(3, 1) != pixel.Ink || d.at(4, 1) != pixel.Back || d.at(3, 2) != pixel.Back || d.at(4, 2) != pixel.Ink {
|
||||
t.Fatal("checker phase follows the element, not the window")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package pixel
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
func TestRuns(t *testing.T) {
|
||||
cases := map[string][][2]int{
|
||||
"##..###.#": {{0, 2}, {4, 7}, {8, 9}},
|
||||
"....": nil,
|
||||
"####": {{0, 4}},
|
||||
".#.#": {{1, 2}, {3, 4}},
|
||||
}
|
||||
for pat, want := range cases {
|
||||
var got [][2]int
|
||||
runs(len(pat), func(i int) bool { return pat[i] == '#' }, func(i, j int) { got = append(got, [2]int{i, j}) })
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("runs(%q) = %v, want %v", pat, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextLinesEllipsis(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
width, n int
|
||||
want []string
|
||||
}{
|
||||
{"one\ntwo\nthree", 0, 2, []string{"one", "two…"}},
|
||||
{"one\ntwo", 0, 2, []string{"one", "two"}},
|
||||
{"hello world again", Measure("hello…"), 1, []string{"hello…"}},
|
||||
{"hello world again", Measure("hel…"), 1, []string{"hel…"}},
|
||||
{"像素风格组件库", 36, 2, []string{"像素风", "格组…"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := textLines(c.s, c.width, c.n); !slices.Equal(got, c.want) {
|
||||
t.Errorf("textLines(%q, %d, %d) = %q, want %q", c.s, c.width, c.n, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fakePainter struct{ fills int }
|
||||
|
||||
func (f *fakePainter) Fill(ui.Rect, ui.Color, float32) { f.fills++ }
|
||||
func (f *fakePainter) After(time.Duration) {}
|
||||
func (f *fakePainter) Now() time.Time { return time.Unix(0, 0) }
|
||||
|
||||
func TestDrawingMergesRuns(t *testing.T) {
|
||||
sp := MustSprite("aab\n.bb")
|
||||
cases := []struct {
|
||||
name string
|
||||
draw func(g Canvas)
|
||||
want int
|
||||
}{
|
||||
{"text A", func(g Canvas) { g.Text(0, 0, "A", Ink, 1) }, 15},
|
||||
{"text A ×2", func(g Canvas) { g.Text(0, 0, "A", Ink, 2) }, 15},
|
||||
{"micro 1", func(g Canvas) { g.Micro(0, 0, "1", Ink, 1) }, 5},
|
||||
{"sprite", func(g Canvas) { g.Sprite(0, 0, sp, 0, Ink, 1) }, 3},
|
||||
{"flat line", func(g Canvas) { g.Line(0, 0, 9, 0, Ink) }, 1},
|
||||
{"steep line", func(g Canvas) { g.Line(0, 0, 0, 9, Ink) }, 1},
|
||||
{"diagonal", func(g Canvas) { g.Line(0, 0, 9, 9, Ink) }, 10},
|
||||
{"circle 3", func(g Canvas) { g.Circle(3, 3, 3, Ink) }, 12},
|
||||
{"disc 3", func(g Canvas) { g.Disc(3, 3, 3, Ink) }, 7},
|
||||
{"dither 0", func(g Canvas) { g.Dither(0, 0, 4, 4, Face, Ink, 0) }, 1},
|
||||
{"dither 8", func(g Canvas) { g.Dither(0, 0, 4, 4, Face, Ink, 8) }, 9},
|
||||
{"dither 16", func(g Canvas) { g.Dither(0, 0, 4, 4, Face, Ink, 16) }, 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
f := &fakePainter{}
|
||||
c.draw(Canvas{p: f, k: Kit{pal: SpringDay, dot: 1}, W: 32, H: 32})
|
||||
if f.fills != c.want {
|
||||
t.Errorf("%s: %d fills, want %d", c.name, f.fills, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package pixel_test
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
// dots reads a Dot 2 frame, dot by dot.
|
||||
type dots struct {
|
||||
t *testing.T
|
||||
img *image.RGBA
|
||||
pal *pixel.Palette
|
||||
}
|
||||
|
||||
// at is the slot of dot x, y.
|
||||
func (d dots) at(x, y int) pixel.Slot {
|
||||
d.t.Helper()
|
||||
c := d.img.RGBAAt(2*x, 2*y)
|
||||
for _, o := range [][2]int{{1, 0}, {0, 1}, {1, 1}} {
|
||||
if d.img.RGBAAt(2*x+o[0], 2*y+o[1]) != c {
|
||||
d.t.Fatalf("dot %d,%d is not one color", x, y)
|
||||
}
|
||||
}
|
||||
for s, pc := range d.pal.Colors {
|
||||
if pc.R == c.R && pc.G == c.G && pc.B == c.B && c.A == 255 {
|
||||
return pixel.Slot(s)
|
||||
}
|
||||
}
|
||||
d.t.Fatalf("dot %d,%d has %v, outside the palette", x, y, c)
|
||||
return 0
|
||||
}
|
||||
|
||||
// count is how many dots of an area hold s.
|
||||
func (d dots) count(x, y, w, h int, s pixel.Slot) int {
|
||||
n := 0
|
||||
for j := range h {
|
||||
for i := range w {
|
||||
if d.at(x+i, y+j) == s {
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// canvas draws on a w×h dot box at the top-left.
|
||||
func canvas(t *testing.T, w, h int, draw func(g pixel.Canvas)) dots {
|
||||
t.Helper()
|
||||
pal := pixel.SummerDay
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pal, Dot: 2})
|
||||
k := pixel.K(c)
|
||||
ui.Box(c).Size(k.D(w), k.D(h)).Draw(func(p *ui.Painter, r ui.Rect) { draw(k.Canvas(p, r)) })
|
||||
}, 2*w, 2*h)
|
||||
return dots{t: t, img: tt.Image(), pal: pal}
|
||||
}
|
||||
+10
-10
@@ -11,12 +11,12 @@ import (
|
||||
type Slot uint8
|
||||
|
||||
const (
|
||||
Back Slot = iota
|
||||
Ink Slot = iota
|
||||
Back
|
||||
Face
|
||||
Lit
|
||||
Shade
|
||||
Edge
|
||||
Ink
|
||||
Faint
|
||||
Well
|
||||
Accent
|
||||
@@ -50,14 +50,14 @@ type Palette struct {
|
||||
func (p *Palette) C(s Slot) ui.Color { return p.Colors[s] }
|
||||
|
||||
var (
|
||||
SpringDay = palette("Spring Day", "春·昼", Spring, false, "F4EEDC E4DCC6 FFFBEF B3A68C 3A2A3E 33283A 6F6358 FFF6E6 B8395C F2A7B9 FFFFFF 4F9A3F D9982A C73A3A 3F7FCB 8C5BC4")
|
||||
SpringNight = palette("Spring Night", "春·夜", Spring, true, "1C1730 2E2850 4C4478 1A1530 E8DFF7 F3EEFF A79DC4 14102A F07EA0 FFC2D2 120E24 7CC66A F2C14E F0605F 6FA8F0 B58AF0")
|
||||
SummerDay = palette("Summer Day", "夏·昼", Summer, false, "F7F3E3 DCEBEE FFFFFF 98B3B8 1D3640 15303B 4F6870 F4FCFF 0E6E8C 7FD0E3 FFFFF6 3A9A4A E0A21A D14836 2F7FD0 9A57C8")
|
||||
SummerNight = palette("Summer Night", "夏·夜", Summer, true, "0B1630 172A55 2C4A86 0A1328 DDEBFF EAF3FF 98ACCF 08102A 36C2E0 A8EEFA 050C1E B8E35A F7D04A F26A5A 5FA8FF C58CFF")
|
||||
AutumnDay = palette("Autumn Day", "秋·昼", Autumn, false, "F2E6D0 E6D2B0 FFF6E2 B08F62 3A2416 33200F 6E5640 FFF1D8 B94A1C F0A060 FFFFFF 6E8F2E C98A12 B8302A 3F78B0 8E4E9E")
|
||||
AutumnNight = palette("Autumn Night", "秋·夜", Autumn, true, "1E1414 33221E 5A3D30 170F0E F1DCC4 FBEBD8 B39A86 140C0B F09A3A FFD08A 0E0808 A8C25A F6C65A F0624E 79A8E0 C88AD8")
|
||||
WinterDay = palette("Winter Day", "冬·昼", Winter, false, "EEF3F8 D8E2EC FFFFFF 98A9BC 1C2A3C 17243A 52637A F6FAFF 1F5FA8 9CC8F0 FCFEFF 3C8C6C B98A1E B23A48 2A86A8 7656B8")
|
||||
WinterNight = palette("Winter Night", "冬·夜", Winter, true, "0A1020 18233F 304674 080C1A E2F2FF EEF7FF 9DB2CF 060A16 7FD8F0 CFF4FF 040814 6FE0A8 F5D47A F07A8A 6FB0FF A994F5")
|
||||
SpringDay = palette("Spring Day", "春·昼", Spring, false, "33283A F4EEDC E4DCC6 FFFBEF B3A68C 3A2A3E 6F6358 FFF6E6 B8395C F2A7B9 FFFFFF 4F9A3F D9982A C73A3A 3F7FCB 8C5BC4")
|
||||
SpringNight = palette("Spring Night", "春·夜", Spring, true, "F3EEFF 1C1730 2E2850 4C4478 1A1530 E8DFF7 A79DC4 14102A F07EA0 FFC2D2 120E24 7CC66A F2C14E F0605F 6FA8F0 B58AF0")
|
||||
SummerDay = palette("Summer Day", "夏·昼", Summer, false, "15303B F7F3E3 DCEBEE FFFFFF 98B3B8 1D3640 4F6870 F4FCFF 0E6E8C 7FD0E3 FFFFF6 3A9A4A E0A21A D14836 2F7FD0 9A57C8")
|
||||
SummerNight = palette("Summer Night", "夏·夜", Summer, true, "EAF3FF 0B1630 172A55 2C4A86 0A1328 DDEBFF 98ACCF 08102A 36C2E0 A8EEFA 050C1E B8E35A F7D04A F26A5A 5FA8FF C58CFF")
|
||||
AutumnDay = palette("Autumn Day", "秋·昼", Autumn, false, "33200F F2E6D0 E6D2B0 FFF6E2 B08F62 3A2416 6E5640 FFF1D8 B94A1C F0A060 FFFFFF 6E8F2E C98A12 B8302A 3F78B0 8E4E9E")
|
||||
AutumnNight = palette("Autumn Night", "秋·夜", Autumn, true, "FBEBD8 1E1414 33221E 5A3D30 170F0E F1DCC4 B39A86 140C0B F09A3A FFD08A 0E0808 A8C25A F6C65A F0624E 79A8E0 C88AD8")
|
||||
WinterDay = palette("Winter Day", "冬·昼", Winter, false, "17243A EEF3F8 D8E2EC FFFFFF 98A9BC 1C2A3C 52637A F6FAFF 1F5FA8 9CC8F0 FCFEFF 3C8C6C B98A1E B23A48 2A86A8 7656B8")
|
||||
WinterNight = palette("Winter Night", "冬·夜", Winter, true, "EEF7FF 0A1020 18233F 304674 080C1A E2F2FF 9DB2CF 060A16 7FD8F0 CFF4FF 040814 6FE0A8 F5D47A F07A8A 6FB0FF A994F5")
|
||||
)
|
||||
|
||||
var Palettes = []*Palette{SpringDay, SpringNight, SummerDay, SummerNight, AutumnDay, AutumnNight, WinterDay, WinterNight}
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ type Kit struct {
|
||||
dot float32
|
||||
locale Locale
|
||||
now time.Time
|
||||
clock func() time.Time
|
||||
reduce bool
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ func K(c *ui.Context) Kit {
|
||||
if s.Now != nil {
|
||||
now = s.Now()
|
||||
}
|
||||
return Kit{pal: s.Palette, dot: float32(s.Dot), locale: s.Locale, now: now, reduce: c.Preferences().ReduceMotion}
|
||||
return Kit{pal: s.Palette, dot: float32(s.Dot), locale: s.Locale, now: now, clock: s.Now, reduce: c.Preferences().ReduceMotion}
|
||||
}
|
||||
|
||||
func frameOf(c *ui.Context) *frame {
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sprite is hand-placed art in palette slots, in frames.
|
||||
type Sprite struct {
|
||||
W, H int
|
||||
frames [][]int8
|
||||
}
|
||||
|
||||
const (
|
||||
cellClear int8 = -1
|
||||
cellInk int8 = -2
|
||||
)
|
||||
|
||||
// Art letters naming slots; '.' is clear, '#' the ink.
|
||||
const artSlots = "kbflsenwaAigyrcm"
|
||||
|
||||
// MustSprite parses string art; blank lines split frames.
|
||||
func MustSprite(art string) *Sprite {
|
||||
var frames [][]string
|
||||
var cur []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(art), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
if cur != nil {
|
||||
frames, cur = append(frames, cur), nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
cur = append(cur, line)
|
||||
}
|
||||
if cur == nil {
|
||||
panic("pixel: empty sprite")
|
||||
}
|
||||
frames = append(frames, cur)
|
||||
sp := &Sprite{W: len(frames[0][0]), H: len(frames[0])}
|
||||
for _, f := range frames {
|
||||
if len(f) != sp.H {
|
||||
panic(fmt.Sprintf("pixel: sprite frames of %d and %d rows", sp.H, len(f)))
|
||||
}
|
||||
px := make([]int8, 0, sp.W*sp.H)
|
||||
for _, row := range f {
|
||||
if len(row) != sp.W {
|
||||
panic(fmt.Sprintf("pixel: sprite row %q is not %d wide", row, sp.W))
|
||||
}
|
||||
for i := range len(row) {
|
||||
px = append(px, cell(row[i]))
|
||||
}
|
||||
}
|
||||
sp.frames = append(sp.frames, px)
|
||||
}
|
||||
return sp
|
||||
}
|
||||
|
||||
func cell(c byte) int8 {
|
||||
switch c {
|
||||
case '.':
|
||||
return cellClear
|
||||
case '#':
|
||||
return cellInk
|
||||
}
|
||||
if i := strings.IndexByte(artSlots, c); i >= 0 {
|
||||
return int8(i)
|
||||
}
|
||||
panic(fmt.Sprintf("pixel: sprite character %q", c))
|
||||
}
|
||||
|
||||
func (s *Sprite) Frames() int { return len(s.frames) }
|
||||
|
||||
// Mirror returns s flipped left to right.
|
||||
func (s *Sprite) Mirror() *Sprite {
|
||||
m := &Sprite{W: s.W, H: s.H}
|
||||
for _, px := range s.frames {
|
||||
out := make([]int8, len(px))
|
||||
for y := range s.H {
|
||||
for x := range s.W {
|
||||
out[y*s.W+x] = px[y*s.W+s.W-1-x]
|
||||
}
|
||||
}
|
||||
m.frames = append(m.frames, out)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Sprite draws frame of sp; # takes ink; frames wrap.
|
||||
func (g Canvas) Sprite(x, y int, sp *Sprite, frame int, ink Slot, scale int) {
|
||||
checkScale(scale)
|
||||
n := len(sp.frames)
|
||||
px := sp.frames[(frame%n+n)%n]
|
||||
for row := range sp.H {
|
||||
line := px[row*sp.W : (row+1)*sp.W]
|
||||
for col := 0; col < sp.W; {
|
||||
v := line[col]
|
||||
end := col + 1
|
||||
for end < sp.W && line[end] == v {
|
||||
end++
|
||||
}
|
||||
if v != cellClear {
|
||||
s := ink
|
||||
if v >= 0 {
|
||||
s = Slot(v)
|
||||
}
|
||||
g.Rect(x+col*scale, y+row*scale, (end-col)*scale, scale, s)
|
||||
}
|
||||
col = end
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user