Add doodad.exe binary and PNG to Drawing Converter
Adds the `doodad` binary which will be a command line tool to work with Doodads and Levels and assist with development. The `doodad` binary has subcommands like git and the first command is `convert` which converts between image files (PNG or BMP) and Doodle drawing files (Level or Doodad). You can "screenshot" a level into a PNG or you can initialize a new drawing from a PNG.
This commit is contained in:
parent
b67c4b67b2
commit
5bf7d554f7
1
Makefile
1
Makefile
|
@ -17,6 +17,7 @@ setup: clean
|
||||||
build:
|
build:
|
||||||
gofmt -w .
|
gofmt -w .
|
||||||
go build $(LDFLAGS) -i -o bin/doodle cmd/doodle/main.go
|
go build $(LDFLAGS) -i -o bin/doodle cmd/doodle/main.go
|
||||||
|
go build $(LDFLAGS) -i -o bin/doodad cmd/doodad/main.go
|
||||||
|
|
||||||
# `make run` to run it in debug mode.
|
# `make run` to run it in debug mode.
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
|
|
|
@ -213,3 +213,8 @@ named:
|
||||||
|
|
||||||
These are the open source **DejaVu Sans [Mono]** fonts, so copy them in from
|
These are the open source **DejaVu Sans [Mono]** fonts, so copy them in from
|
||||||
your `/usr/share/fonts/dejavu` folder or provide alternative fonts.
|
your `/usr/share/fonts/dejavu` folder or provide alternative fonts.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir fonts
|
||||||
|
cp /usr/share/fonts/dejavu/{DejaVuSans.ttf,DejaVuSans-Bold.ttf,DejaVuSansMono.ttf} fonts/
|
||||||
|
```
|
||||||
|
|
43
cmd/doodad/README.md
Normal file
43
cmd/doodad/README.md
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# doodad.exe
|
||||||
|
|
||||||
|
The doodad tool is a command line interface for interacting with Levels and
|
||||||
|
Doodad files, collectively referred to as "Doodle drawings" or just "drawings"
|
||||||
|
for short.
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
|
||||||
|
## doodad convert
|
||||||
|
|
||||||
|
Convert between standard image files (bitmap or PNG) and Doodle drawings
|
||||||
|
(levels or doodads).
|
||||||
|
|
||||||
|
This command can be used to "export" a Doodle drawing as a PNG (when run against
|
||||||
|
a Level file, it may export a massive PNG image containing the entire level).
|
||||||
|
It may also "import" a new Doodle drawing from an image on disk.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export a full screenshot of your level
|
||||||
|
$ doodad convert mymap.level screenshot.png
|
||||||
|
|
||||||
|
# Create a new level based from a PNG image.
|
||||||
|
$ doodad convert scanned-drawing.png new-level.level
|
||||||
|
|
||||||
|
# Create a new doodad based from a BMP image, and in this image the chroma
|
||||||
|
# color (transparent) is #FF00FF instead of white as default.
|
||||||
|
$ doodad convert --key '#FF00FF' button.png button.doodad
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported image types:
|
||||||
|
|
||||||
|
* PNG (8-bit or 24-bit, with transparent pixels or chroma key)
|
||||||
|
* BMP (bitmap image with chroma key)
|
||||||
|
|
||||||
|
The chrome key defaults to white (`#FFFFFF`), so pixels of that color are
|
||||||
|
treated as transparent and ignored. For PNG images, if a pixel is fully
|
||||||
|
transparent (alpha channel 0%) it will also be skipped.
|
||||||
|
|
||||||
|
When converting an image into a drawing, the unique colors identified in the
|
||||||
|
drawing are extracted into the palette. You will need to later edit the palette
|
||||||
|
to assign meaning to the colors.
|
8
cmd/doodad/commands/consts.go
Normal file
8
cmd/doodad/commands/consts.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
const (
|
||||||
|
extLevel = ".level"
|
||||||
|
extDoodad = ".doodad"
|
||||||
|
extPNG = ".png"
|
||||||
|
extBMP = ".bmp"
|
||||||
|
)
|
269
cmd/doodad/commands/convert.go
Normal file
269
cmd/doodad/commands/convert.go
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"image/png"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle"
|
||||||
|
"git.kirsle.net/apps/doodle/doodads"
|
||||||
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
"golang.org/x/image/bmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Convert between image files (png or bitmap) and Doodle drawing files (levels
|
||||||
|
// and doodads)
|
||||||
|
var Convert cli.Command
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Convert = cli.Command{
|
||||||
|
Name: "convert",
|
||||||
|
Usage: "convert between images and Doodle drawing files",
|
||||||
|
ArgsUsage: "<input> <output>",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "chroma key color for transparency on input image files",
|
||||||
|
Value: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
if c.NArg() != 2 {
|
||||||
|
return cli.NewExitError(
|
||||||
|
"Usage: doodad convert <input.png> <output.level>\n"+
|
||||||
|
" Image file types: png, bmp\n"+
|
||||||
|
" Drawing file types: level, doodad",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the chroma key.
|
||||||
|
chroma, err := render.HexColor(c.String("key"))
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(
|
||||||
|
"Chrome key not a valid color: "+err.Error(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := c.Args()
|
||||||
|
var (
|
||||||
|
inputFile = args[0]
|
||||||
|
inputType = strings.ToLower(filepath.Ext(inputFile))
|
||||||
|
outputFile = args[1]
|
||||||
|
outputType = strings.ToLower(filepath.Ext(outputFile))
|
||||||
|
)
|
||||||
|
|
||||||
|
if inputType == extPNG || inputType == extBMP {
|
||||||
|
if outputType == extLevel || outputType == extDoodad {
|
||||||
|
if err := imageToDrawing(c, chroma, inputFile, outputFile); err != nil {
|
||||||
|
return cli.NewExitError(err.Error(), 1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cli.NewExitError("Image inputs can only output to Doodle drawings", 1)
|
||||||
|
} else if inputType == extLevel || inputType == extDoodad {
|
||||||
|
if outputType == extPNG || outputType == extBMP {
|
||||||
|
if err := drawingToImage(c, chroma, inputFile, outputFile); err != nil {
|
||||||
|
return cli.NewExitError(err.Error(), 1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cli.NewExitError("Doodle drawing inputs can only output to image files", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.NewExitError("File types must be: png, bmp, level, doodad", 1)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageToDrawing(c *cli.Context, chroma render.Color, inputFile, outputFile string) error {
|
||||||
|
reader, err := os.Open(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, format, err := image.Decode(reader)
|
||||||
|
log.Info("format: %s", format)
|
||||||
|
_ = img
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err.Error(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the bounding box information of the source image.
|
||||||
|
var (
|
||||||
|
bounds = img.Bounds()
|
||||||
|
imageSize = bounds.Size()
|
||||||
|
chunkSize int // the square shape for Doodad chunk size
|
||||||
|
)
|
||||||
|
if imageSize.X > imageSize.Y {
|
||||||
|
chunkSize = imageSize.X
|
||||||
|
} else {
|
||||||
|
chunkSize = imageSize.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the output drawing file.
|
||||||
|
switch strings.ToLower(filepath.Ext(outputFile)) {
|
||||||
|
case extDoodad:
|
||||||
|
log.Info("Output is a Doodad file (chunk size %d): %s", chunkSize, outputFile)
|
||||||
|
doodad := doodads.New(chunkSize)
|
||||||
|
doodad.GameVersion = doodle.Version
|
||||||
|
doodad.Title = "Converted Doodad"
|
||||||
|
doodad.Author = os.Getenv("USER")
|
||||||
|
doodad.Palette = imageToChunker(img, chroma, doodad.Layers[0].Chunker)
|
||||||
|
|
||||||
|
err := doodad.WriteJSON(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err.Error(), 1)
|
||||||
|
}
|
||||||
|
case extLevel:
|
||||||
|
log.Info("Output is a Level file: %s", outputFile)
|
||||||
|
|
||||||
|
lvl := level.New()
|
||||||
|
lvl.GameVersion = doodle.Version
|
||||||
|
lvl.Title = "Converted Level"
|
||||||
|
lvl.Author = os.Getenv("USER")
|
||||||
|
lvl.Palette = imageToChunker(img, chroma, lvl.Chunker)
|
||||||
|
|
||||||
|
err := lvl.WriteJSON(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err.Error(), 1)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return cli.NewExitError("invalid output file: not a Doodle drawing", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawingToImage(c *cli.Context, chroma render.Color, inputFile, outputFile string) error {
|
||||||
|
var palette *level.Palette
|
||||||
|
var chunker *level.Chunker
|
||||||
|
|
||||||
|
switch strings.ToLower(filepath.Ext(inputFile)) {
|
||||||
|
case extLevel:
|
||||||
|
log.Info("Load Level: %s", inputFile)
|
||||||
|
m, err := level.LoadJSON(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load level: %s", err.Error())
|
||||||
|
}
|
||||||
|
chunker = m.Chunker
|
||||||
|
palette = m.Palette
|
||||||
|
case extDoodad:
|
||||||
|
log.Info("Load Doodad: %s", inputFile)
|
||||||
|
d, err := doodads.LoadJSON(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load doodad: %s", err.Error())
|
||||||
|
}
|
||||||
|
chunker = d.Layers[0].Chunker // TODO: layers
|
||||||
|
palette = d.Palette
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%s: not a level or doodad file", inputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = chunker
|
||||||
|
_ = palette
|
||||||
|
|
||||||
|
// Create an image for the full world size.
|
||||||
|
canvas := chunker.WorldSizePositive()
|
||||||
|
img := image.NewRGBA(image.Rectangle{
|
||||||
|
Min: image.Point{
|
||||||
|
X: int(canvas.X),
|
||||||
|
Y: int(canvas.Y),
|
||||||
|
},
|
||||||
|
Max: image.Point{
|
||||||
|
X: int(canvas.W),
|
||||||
|
Y: int(canvas.H),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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, render.White.ToColor())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transcode all pixels onto it.
|
||||||
|
for px := range chunker.IterPixels() {
|
||||||
|
img.Set(int(px.X), int(px.Y), px.Swatch.Color.ToColor())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the output file.
|
||||||
|
switch strings.ToLower(filepath.Ext(outputFile)) {
|
||||||
|
case ".png":
|
||||||
|
fh, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
return png.Encode(fh, img)
|
||||||
|
case ".bmp":
|
||||||
|
fh, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
return bmp.Encode(fh, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("not valid output image type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageToChunker implements a generic transcoding of an image.Image to a Chunker
|
||||||
|
// and returns the Palette, ready to plug into a Doodad or Level drawing.
|
||||||
|
//
|
||||||
|
// img: input image like a PNG
|
||||||
|
// chroma: transparent color
|
||||||
|
func imageToChunker(img image.Image, chroma render.Color, chunker *level.Chunker) *level.Palette {
|
||||||
|
var (
|
||||||
|
palette = level.NewPalette()
|
||||||
|
bounds = img.Bounds()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache a palette of unique colors as we go.
|
||||||
|
var uniqueColor = map[string]*level.Swatch{}
|
||||||
|
|
||||||
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
px := img.At(x, y)
|
||||||
|
color := render.FromColor(px)
|
||||||
|
if color == chroma || color.Transparent() { // invisible pixel
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// New color for the palette?
|
||||||
|
swatch, ok := uniqueColor[color.String()]
|
||||||
|
if !ok {
|
||||||
|
log.Info("New color: %s", color)
|
||||||
|
swatch = &level.Swatch{
|
||||||
|
Name: color.String(),
|
||||||
|
Color: color,
|
||||||
|
}
|
||||||
|
uniqueColor[color.String()] = swatch
|
||||||
|
}
|
||||||
|
|
||||||
|
chunker.Set(render.NewPoint(int32(x), int32(y)), swatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order the palette.
|
||||||
|
var sortedColors []string
|
||||||
|
for k := range uniqueColor {
|
||||||
|
sortedColors = append(sortedColors, k)
|
||||||
|
}
|
||||||
|
sort.Strings(sortedColors)
|
||||||
|
for _, hex := range sortedColors {
|
||||||
|
palette.Swatches = append(palette.Swatches, uniqueColor[hex])
|
||||||
|
}
|
||||||
|
|
||||||
|
return palette
|
||||||
|
}
|
14
cmd/doodad/commands/log.go
Normal file
14
cmd/doodad/commands/log.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import "github.com/kirsle/golog"
|
||||||
|
|
||||||
|
var log *golog.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = golog.GetLogger("doodad")
|
||||||
|
log.Configure(&golog.Config{
|
||||||
|
Level: golog.InfoLevel,
|
||||||
|
Theme: golog.DarkTheme,
|
||||||
|
Colors: golog.ExtendedColor,
|
||||||
|
})
|
||||||
|
}
|
40
cmd/doodad/main.go
Normal file
40
cmd/doodad/main.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// doodad is the command line developer tool for Doodle.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle"
|
||||||
|
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Build = "N/A"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "doodad"
|
||||||
|
app.Usage = "command line interface for Doodle"
|
||||||
|
app.Version = doodle.Version + " build " + Build
|
||||||
|
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "debug, d",
|
||||||
|
Usage: "enable debug level logging",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
commands.Convert,
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(cli.FlagsByName(app.Flags))
|
||||||
|
sort.Sort(cli.CommandsByName(app.Commands))
|
||||||
|
|
||||||
|
err := app.Run(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,6 +81,53 @@ func (c *Chunker) IterPixels() <-chan Pixel {
|
||||||
return pipe
|
return pipe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorldSize returns the bounding coordinates that the Chunker has chunks to
|
||||||
|
// manage: the lowest pixels from the lowest chunks to the highest pixels of
|
||||||
|
// the highest chunks.
|
||||||
|
func (c *Chunker) WorldSize() render.Rect {
|
||||||
|
// Lowest and highest chunks.
|
||||||
|
var (
|
||||||
|
chunkLowest render.Point
|
||||||
|
chunkHighest render.Point
|
||||||
|
size = int32(c.Size)
|
||||||
|
)
|
||||||
|
|
||||||
|
for coord := range c.Chunks {
|
||||||
|
if coord.X < chunkLowest.X {
|
||||||
|
chunkLowest.X = coord.X
|
||||||
|
}
|
||||||
|
if coord.Y < chunkLowest.Y {
|
||||||
|
chunkLowest.Y = coord.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
if coord.X > chunkHighest.X {
|
||||||
|
chunkHighest.X = coord.X
|
||||||
|
}
|
||||||
|
if coord.Y > chunkHighest.Y {
|
||||||
|
chunkHighest.Y = coord.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return render.Rect{
|
||||||
|
X: chunkLowest.X * size,
|
||||||
|
Y: chunkLowest.Y * size,
|
||||||
|
W: (chunkHighest.X * size) + (size - 1),
|
||||||
|
H: (chunkHighest.Y * size) + (size - 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorldSizePositive returns the WorldSize anchored to 0,0 with only positive
|
||||||
|
// coordinates.
|
||||||
|
func (c *Chunker) WorldSizePositive() render.Rect {
|
||||||
|
S := c.WorldSize()
|
||||||
|
return render.Rect{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
W: int32(math.Abs(float64(S.X))) + S.W,
|
||||||
|
H: int32(math.Abs(float64(S.Y))) + S.H,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetChunk gets a chunk at a certain position. Returns false if not found.
|
// GetChunk gets a chunk at a certain position. Returns false if not found.
|
||||||
func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) {
|
func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) {
|
||||||
chunk, ok := c.Chunks[p]
|
chunk, ok := c.Chunks[p]
|
||||||
|
|
85
level/chunker_test.go
Normal file
85
level/chunker_test.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package level_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWorldSize(t *testing.T) {
|
||||||
|
type TestCase struct {
|
||||||
|
Size int
|
||||||
|
Points []render.Point
|
||||||
|
Expect render.Rect
|
||||||
|
Zero render.Rect // expected WorldSizePositive
|
||||||
|
}
|
||||||
|
var tests = []TestCase{
|
||||||
|
{
|
||||||
|
Size: 1000,
|
||||||
|
Points: []render.Point{
|
||||||
|
render.NewPoint(0, 0), // chunk 0,0
|
||||||
|
render.NewPoint(512, 788), // 0,0
|
||||||
|
render.NewPoint(1002, 500), // 1,0
|
||||||
|
render.NewPoint(2005, 2006), // 2,2
|
||||||
|
render.NewPoint(-5, -5), // -1,-1
|
||||||
|
},
|
||||||
|
Expect: render.Rect{
|
||||||
|
X: -1000,
|
||||||
|
Y: -1000,
|
||||||
|
W: 2999,
|
||||||
|
H: 2999,
|
||||||
|
},
|
||||||
|
Zero: render.NewRect(3999, 3999),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Size: 128,
|
||||||
|
Points: []render.Point{
|
||||||
|
render.NewPoint(5, 5),
|
||||||
|
},
|
||||||
|
Expect: render.Rect{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
W: 127,
|
||||||
|
H: 127,
|
||||||
|
},
|
||||||
|
Zero: render.NewRect(127, 127),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Size: 200,
|
||||||
|
Points: []render.Point{
|
||||||
|
render.NewPoint(-6000, -38556),
|
||||||
|
render.NewPoint(12345, 1288000),
|
||||||
|
},
|
||||||
|
Expect: render.Rect{
|
||||||
|
X: -6000,
|
||||||
|
Y: -38600,
|
||||||
|
W: 12399,
|
||||||
|
H: 1288199,
|
||||||
|
},
|
||||||
|
Zero: render.NewRect(18399, 1326799),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
c := level.NewChunker(test.Size)
|
||||||
|
sw := &level.Swatch{
|
||||||
|
Name: "solid",
|
||||||
|
Color: render.Black,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pt := range test.Points {
|
||||||
|
c.Set(pt, sw)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := c.WorldSize()
|
||||||
|
if size != test.Expect {
|
||||||
|
t.Errorf("WorldSize not as expected: %s <> %s", size, test.Expect)
|
||||||
|
}
|
||||||
|
|
||||||
|
zero := c.WorldSizePositive()
|
||||||
|
if zero != test.Zero {
|
||||||
|
t.Errorf("WorldSizePositive not as expected: %s <> %s", zero, test.Expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,13 +19,15 @@ func (m *Level) ToJSON() ([]byte, error) {
|
||||||
|
|
||||||
// WriteJSON writes a level to JSON on disk.
|
// WriteJSON writes a level to JSON on disk.
|
||||||
func (m *Level) WriteJSON(filename string) error {
|
func (m *Level) WriteJSON(filename string) error {
|
||||||
fh, err := os.Create(filename)
|
json, err := m.ToJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Level.WriteJSON(%s): failed to create file: %s", filename, err)
|
return fmt.Errorf("Level.WriteJSON: JSON encode error: %s", err)
|
||||||
}
|
}
|
||||||
defer fh.Close()
|
|
||||||
|
|
||||||
_ = fh
|
err = ioutil.WriteFile(filename, json, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Level.WriteJSON: WriteFile error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
@ -36,6 +37,22 @@ func RGBA(r, g, b, a uint8) Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromColor creates a render.Color from a Go color.Color
|
||||||
|
func FromColor(from color.Color) Color {
|
||||||
|
// downscale a 16-bit color value to 8-bit. input range 0x0000..0xffff
|
||||||
|
downscale := func(in uint32) uint8 {
|
||||||
|
var scale = float64(in) / 0xffff
|
||||||
|
return uint8(scale * 0xff)
|
||||||
|
}
|
||||||
|
r, g, b, a := from.RGBA()
|
||||||
|
return RGBA(
|
||||||
|
downscale(r),
|
||||||
|
downscale(g),
|
||||||
|
downscale(b),
|
||||||
|
downscale(a),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// MustHexColor parses a color from hex code or panics.
|
// MustHexColor parses a color from hex code or panics.
|
||||||
func MustHexColor(hex string) Color {
|
func MustHexColor(hex string) Color {
|
||||||
color, err := HexColor(hex)
|
color, err := HexColor(hex)
|
||||||
|
@ -94,6 +111,22 @@ func (c Color) String() string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToColor converts a render.Color into a Go standard color.Color
|
||||||
|
func (c Color) ToColor() color.RGBA {
|
||||||
|
return color.RGBA{
|
||||||
|
R: c.Red,
|
||||||
|
G: c.Green,
|
||||||
|
B: c.Blue,
|
||||||
|
A: c.Alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transparent returns whether the alpha channel is zeroed out and the pixel
|
||||||
|
// won't appear as anything when rendered.
|
||||||
|
func (c Color) Transparent() bool {
|
||||||
|
return c.Alpha == 0x00
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON serializes the Color for JSON.
|
// MarshalJSON serializes the Color for JSON.
|
||||||
func (c Color) MarshalJSON() ([]byte, error) {
|
func (c Color) MarshalJSON() ([]byte, error) {
|
||||||
return []byte(fmt.Sprintf(
|
return []byte(fmt.Sprintf(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user