Add canvas, dither, sprites, clock and pixel elements

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 16:02:31 -04:00
1 parent 538031af31
commit d33bf0cbbb
11 files changed
+1274 -11

No files matched your search

+271
View File
@@ -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
}