Files
8bitMyGoUI/pixel/canvas.go
T

272 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}