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, } }