pixel-sprite-maker: spritec - .sprite text format -> png/jpg/svg + sprite sheets
- .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
This commit is contained in:
75
pixel-sprite-maker/sprite/render.go
Normal file
75
pixel-sprite-maker/sprite/render.go
Normal file
@@ -0,0 +1,75 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user