- .sprite format: single-char palette keys (hex/rgb()/CSS names/none), grid rows, '.' transparent by default, max 256x256 per sprite - render/sheet/info/preview subcommands; sheets name themselves <base>_<cellW>x<cellH>_<cols>x<rows>.<ext> (row-major) - SVG output RLE-merges pixel runs; JPG composites over --bg - go tests for parser, colors, scaling, svg, sheet layout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmdG9GqfSWCzts7AkDwRDh
76 lines
1.9 KiB
Go
76 lines
1.9 KiB
Go
package sprite
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
)
|
|
|
|
// PixelGrid is anything that can be rendered pixel by pixel: a single
|
|
// Sprite or a composed Sheet.
|
|
type PixelGrid interface {
|
|
Bounds() (w, h int)
|
|
At(x, y int) color.NRGBA
|
|
}
|
|
|
|
// Image renders a PixelGrid to an NRGBA image, scaled up by the integer
|
|
// factor scale (nearest neighbour, keeps pixels crisp).
|
|
func Image(g PixelGrid, scale int) *image.NRGBA {
|
|
if scale < 1 {
|
|
scale = 1
|
|
}
|
|
w, h := g.Bounds()
|
|
img := image.NewNRGBA(image.Rect(0, 0, w*scale, h*scale))
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
c := g.At(x, y)
|
|
for dy := 0; dy < scale; dy++ {
|
|
for dx := 0; dx < scale; dx++ {
|
|
img.SetNRGBA(x*scale+dx, y*scale+dy, c)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return img
|
|
}
|
|
|
|
// WritePNG encodes g as PNG with transparency preserved.
|
|
func WritePNG(w io.Writer, g PixelGrid, scale int) error {
|
|
return png.Encode(w, Image(g, scale))
|
|
}
|
|
|
|
// WriteJPG encodes g as JPEG. JPEG has no alpha channel, so transparent
|
|
// pixels are composited over bg first.
|
|
func WriteJPG(w io.Writer, g PixelGrid, scale int, bg color.NRGBA, quality int) error {
|
|
if quality < 1 || quality > 100 {
|
|
return fmt.Errorf("jpg quality %d out of range 1-100", quality)
|
|
}
|
|
src := Image(g, scale)
|
|
b := src.Bounds()
|
|
flat := image.NewRGBA(b)
|
|
for y := b.Min.Y; y < b.Max.Y; y++ {
|
|
for x := b.Min.X; x < b.Max.X; x++ {
|
|
flat.Set(x, y, blendOver(src.NRGBAAt(x, y), bg))
|
|
}
|
|
}
|
|
return jpeg.Encode(w, flat, &jpeg.Options{Quality: quality})
|
|
}
|
|
|
|
// blendOver composites src over an opaque background color.
|
|
func blendOver(src, bg color.NRGBA) color.NRGBA {
|
|
if src.A == 255 {
|
|
return src
|
|
}
|
|
a := uint32(src.A)
|
|
inv := 255 - a
|
|
return color.NRGBA{
|
|
R: uint8((uint32(src.R)*a + uint32(bg.R)*inv) / 255),
|
|
G: uint8((uint32(src.G)*a + uint32(bg.G)*inv) / 255),
|
|
B: uint8((uint32(src.B)*a + uint32(bg.B)*inv) / 255),
|
|
A: 255,
|
|
}
|
|
}
|