package level import ( "encoding/json" "fmt" "image" "math" "os" "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/userdir" "github.com/satori/go.uuid" "golang.org/x/image/bmp" ) // Types of chunks. const ( MapType int = iota GridType ) // Chunk holds a single portion of the pixel canvas. type Chunk struct { Type int // map vs. 2D array. Accessor // Values told to it from higher up, not stored in JSON. Point render.Point Size int // Texture cache properties so we don't redraw pixel-by-pixel every frame. uuid uuid.UUID texture render.Texturer textureMasked render.Texturer dirty bool } // JSONChunk holds a lightweight (interface-free) copy of the Chunk for // unmarshalling JSON files from disk. type JSONChunk struct { Type int `json:"type"` Data json.RawMessage `json:"data"` } // Accessor provides a high-level API to interact with absolute pixel coordinates // while abstracting away the details of how they're stored. type Accessor interface { Inflate(*Palette) error Iter() <-chan Pixel IterViewport(viewport render.Rect) <-chan Pixel Get(render.Point) (*Swatch, error) Set(render.Point, *Swatch) error Delete(render.Point) error Len() int MarshalJSON() ([]byte, error) UnmarshalJSON([]byte) error } // NewChunk creates a new chunk. func NewChunk() *Chunk { return &Chunk{ Type: MapType, Accessor: NewMapAccessor(), } } // Texture will return a cached texture for the rendering engine for this // chunk's pixel data. If the cache is dirty it will be rebuilt in this func. func (c *Chunk) Texture(e render.Engine) render.Texturer { if c.texture == nil || c.dirty { // Generate the normal bitmap and one with a color mask if applicable. bitmap := c.toBitmap(render.Invisible) defer os.Remove(bitmap) tex, err := e.NewBitmap(bitmap) if err != nil { log.Error("Texture: %s", err) } c.texture = tex c.textureMasked = nil // invalidate until next call c.dirty = false } return c.texture } // TextureMasked returns a cached texture with the ColorMask applied. func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texturer { if c.textureMasked == nil { // Generate the normal bitmap and one with a color mask if applicable. bitmap := c.toBitmap(mask) defer os.Remove(bitmap) tex, err := e.NewBitmap(bitmap) if err != nil { log.Error("Texture: %s", err) } c.textureMasked = tex } return c.textureMasked } // toBitmap puts the texture in a well named bitmap path in the cache folder. func (c *Chunk) toBitmap(mask render.Color) string { // Generate a unique filename for this chunk cache. var filename string if c.uuid == uuid.Nil { c.uuid = uuid.Must(uuid.NewV4()) } filename = c.uuid.String() if mask != render.Invisible { filename += fmt.Sprintf("-%02x%02x%02x%02x", mask.Red, mask.Green, mask.Blue, mask.Alpha, ) } // Get the temp bitmap image. bitmap := userdir.CacheFilename("chunk", filename+".bmp") err := c.ToBitmap(bitmap, mask) if err != nil { log.Error("Texture: %s", err) } return bitmap } // ToBitmap exports the chunk's pixels as a bitmap image. func (c *Chunk) ToBitmap(filename string, mask render.Color) error { canvas := c.SizePositive() imgSize := image.Rectangle{ Min: image.Point{}, Max: image.Point{ X: c.Size, Y: c.Size, }, } if imgSize.Max.X == 0 { imgSize.Max.X = int(canvas.W) } if imgSize.Max.Y == 0 { imgSize.Max.Y = int(canvas.H) } img := image.NewRGBA(imgSize) // Blank out the pixels. for x := 0; x < img.Bounds().Max.X; x++ { for y := 0; y < img.Bounds().Max.Y; y++ { img.Set(x, y, balance.DebugChunkBitmapBackground.ToColor()) } } // Pixel coordinate offset to map the Chunk World Position to the // smaller image boundaries. pointOffset := render.Point{ X: int32(c.Point.X * int32(c.Size)), Y: int32(c.Point.Y * int32(c.Size)), } // Blot all the pixels onto it. for px := range c.Iter() { var color = px.Swatch.Color if mask != render.Invisible { color = mask } img.Set( int(px.X-pointOffset.X), int(px.Y-pointOffset.Y), color.ToColor(), ) } fh, err := os.Create(filename) if err != nil { return err } defer fh.Close() return bmp.Encode(fh, img) } // Set proxies to the accessor and flags the texture as dirty. func (c *Chunk) Set(p render.Point, sw *Swatch) error { c.dirty = true return c.Accessor.Set(p, sw) } // Delete proxies to the accessor and flags the texture as dirty. func (c *Chunk) Delete(p render.Point) error { c.dirty = true return c.Accessor.Delete(p) } // Rect returns the bounding coordinates that the Chunk has pixels for. func (c *Chunk) Rect() render.Rect { // Lowest and highest chunks. var ( lowest render.Point highest render.Point ) for coord := range c.Iter() { if coord.X < lowest.X { lowest.X = coord.X } if coord.Y < lowest.Y { lowest.Y = coord.Y } if coord.X > highest.X { highest.X = coord.X } if coord.Y > highest.Y { highest.Y = coord.Y } } return render.Rect{ X: lowest.X, Y: lowest.Y, W: highest.X, H: highest.Y, } } // SizePositive returns the Size anchored to 0,0 with only positive // coordinates. func (c *Chunk) SizePositive() render.Rect { S := c.Rect() return render.Rect{ W: int32(math.Abs(float64(S.X))) + S.W, H: int32(math.Abs(float64(S.Y))) + S.H, } } // Usage returns the percent of free space vs. allocated pixels in the chunk. func (c *Chunk) Usage(size int) float64 { return float64(c.Len()) / float64(size) } // MarshalJSON writes the chunk to JSON. func (c *Chunk) MarshalJSON() ([]byte, error) { data, err := c.Accessor.MarshalJSON() if err != nil { return []byte{}, err } generic := &JSONChunk{ Type: c.Type, Data: data, } b, err := json.Marshal(generic) return b, err } // UnmarshalJSON loads the chunk from JSON and uses the correct accessor to // parse the inner details. func (c *Chunk) UnmarshalJSON(b []byte) error { // Parse it generically so we can hand off the inner "data" object to the // right accessor for unmarshalling. generic := &JSONChunk{} err := json.Unmarshal(b, generic) if err != nil { return fmt.Errorf("Chunk.UnmarshalJSON: failed to unmarshal into generic JSONChunk type: %s", err) } switch c.Type { case MapType: c.Accessor = NewMapAccessor() return c.Accessor.UnmarshalJSON(generic.Data) default: return fmt.Errorf("Chunk.UnmarshalJSON: unsupported chunk type '%d'", c.Type) } }