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
|
||||
}
|
||||
Reference in new issue
Block a user