diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index 69a2f33..e84ddb2 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -8,8 +8,10 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/enum" + "git.kirsle.net/apps/doodle/pkg/level/giant_screenshot" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/native" + "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" @@ -121,6 +123,20 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { levelMenu.AddItemAccel("Playtest", "P", func() { u.Scene.Playtest() }) + + levelMenu.AddSeparator() + levelMenu.AddItem("Giant Screenshot", func() { + filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level) + if err != nil { + d.Flash(err.Error()) + return + } + + d.Flash("Saved screenshot to: %s", filename) + }) + levelMenu.AddItem("Open screenshot folder", func() { + native.OpenLocalURL(userdir.ScreenshotDirectory) + }) } //////// diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index 0d5ebed..569691f 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -131,34 +131,12 @@ func (c *Chunker) IterPixels() <-chan Pixel { // 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 = 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 - } - } - + chunkLowest, chunkHighest := c.Bounds() return render.Rect{ - X: chunkLowest.X * size, - Y: chunkLowest.Y * size, - W: (chunkHighest.X * size) + (size - 1), - H: (chunkHighest.Y * size) + (size - 1), + X: chunkLowest.X * c.Size, + Y: chunkLowest.Y * c.Size, + W: (chunkHighest.X * c.Size) + (c.Size - 1), + H: (chunkHighest.Y * c.Size) + (c.Size - 1), } } @@ -174,6 +152,28 @@ func (c *Chunker) WorldSizePositive() render.Rect { } } +// Bounds returns the boundary points of the lowest and highest chunk which +// have any data in them. +func (c *Chunker) Bounds() (low, high render.Point) { + for coord := range c.Chunks { + if coord.X < low.X { + low.X = coord.X + } + if coord.Y < low.Y { + low.Y = coord.Y + } + + if coord.X > high.X { + high.X = coord.X + } + if coord.Y > high.Y { + high.Y = coord.Y + } + } + + return low, high +} + // GetChunk gets a chunk at a certain position. Returns false if not found. func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) { chunk, ok := c.Chunks[p] diff --git a/pkg/level/giant_screenshot/giant_screenshot.go b/pkg/level/giant_screenshot/giant_screenshot.go new file mode 100644 index 0000000..f950573 --- /dev/null +++ b/pkg/level/giant_screenshot/giant_screenshot.go @@ -0,0 +1,180 @@ +package giant_screenshot + +import ( + "image" + "image/draw" + "image/png" + "os" + "path/filepath" + "time" + + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/userdir" + "git.kirsle.net/apps/doodle/pkg/wallpaper" + "git.kirsle.net/go/render" +) + +/* +Giant Screenshot functionality for the Level Editor. +*/ + +// GiantScreenshot returns a rendered RGBA image of the entire level. +func GiantScreenshot(lvl *level.Level) image.Image { + // How big will our image be? + var ( + size = lvl.Chunker.WorldSizePositive() + chunkSize = lvl.Chunker.Size + chunkLow, chunkHigh = lvl.Chunker.Bounds() + worldSize = render.Rect{ + X: chunkLow.X, + Y: chunkLow.Y, + W: chunkHigh.X, + H: chunkHigh.Y, + } + x int + y int + ) + + // Bounded levels: set the image output size precisely. + if lvl.PageType == level.Bounded || lvl.PageType == level.Bordered { + size = render.NewRect(int(lvl.MaxWidth), int(lvl.MaxHeight)) + } + + // Levels without negative space: set the lower chunk coord to 0,0 + if lvl.PageType > level.Unbounded { + worldSize.X = 0 + worldSize.Y = 0 + } + + // Create the image. + img := image.NewRGBA(image.Rect(0, 0, size.W, size.H)) + + // Render the wallpaper onto it. + log.Debug("GiantScreenshot: Render wallpaper to image (%s)...", size) + img = WallpaperToImage(lvl, img, size.W, size.H) + + // Render the chunks. + log.Debug("GiantScreenshot: Render level chunks...") + for chunkX := worldSize.X; chunkX <= worldSize.W; chunkX++ { + y = 0 + for chunkY := worldSize.Y; chunkY <= worldSize.H; chunkY++ { + if chunk, ok := lvl.Chunker.GetChunk(render.NewPoint(chunkX, chunkY)); ok { + // TODO: we always use RGBA but is risky: + rgba, ok := chunk.CachedBitmap(render.Invisible).(*image.RGBA) + if !ok { + log.Error("GiantScreenshot: couldn't turn chunk to RGBA") + } + img = blotImage(img, rgba, image.Pt(x, y)) + } + y += chunkSize + } + x += chunkSize + } + + // Render the doodads. + log.Debug("GiantScreenshot: Render actors...") + for _, actor := range lvl.Actors { + doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl) + if err != nil { + log.Error("GiantScreenshot: Load doodad: %s", err) + continue + } + + // Offset the doodad position if the image is displaying + // negative coordinates. + if worldSize.X < 0 { + actor.Point.X += render.AbsInt(worldSize.X) * chunkSize + } + if worldSize.Y < 0 { + actor.Point.Y += render.AbsInt(worldSize.Y) * chunkSize + } + + // TODO: usually doodad sprites start at 0,0 and the chunkSize + // is the same as their sprite size. + if len(doodad.Layers) > 0 && doodad.Layers[0].Chunker != nil { + var chunker = doodad.Layers[0].Chunker + chunk, ok := chunker.GetChunk(render.Origin) + if !ok { + continue + } + + // TODO: we always use RGBA but is risky: + rgba, ok := chunk.CachedBitmap(render.Invisible).(*image.RGBA) + if !ok { + log.Error("GiantScreenshot: couldn't turn chunk to RGBA") + } + img = blotImage(img, rgba, image.Pt(actor.Point.X, actor.Point.Y)) + } + + } + + return img +} + +// SaveGiantScreenshot will take a screenshot and write it to a file on disk, +// returning the filename relative to ~/.config/doodle/screenshots +func SaveGiantScreenshot(level *level.Level) (string, error) { + var filename = time.Now().Format("2006-01-02_15-04-05.png") + + img := GiantScreenshot(level) + + fh, err := os.Create(filepath.Join(userdir.ScreenshotDirectory, filename)) + if err != nil { + return "", err + } + + png.Encode(fh, img) + return filename, nil +} + +// WallpaperToImage accurately draws the wallpaper into an Image. +// +// The image is assumed to have a rect of (0,0,width,height) and that +// width and height are positive. Used for the Giant Screenshot feature. +func WallpaperToImage(lvl *level.Level, target *image.RGBA, width, height int) *image.RGBA { + wp, err := wallpaper.FromFile("assets/wallpapers/"+lvl.Wallpaper, lvl) + if err != nil { + log.Error("GiantScreenshot: wallpaper load: %s", err) + } + + var size = wp.QuarterRect() + var resultImage = target + + // Tile the repeat texture. + for x := 0; x < width; x += size.W { + for y := 0; y < height; y += size.H { + offset := image.Pt(x, y) + resultImage = blotImage(resultImage, wp.Repeat(), offset) + } + } + + // Tile the left edge for bounded lvls. + if lvl.PageType > level.Unbounded { + // The left edge. + for y := 0; y < height; y += size.H { + offset := image.Pt(0, y) + resultImage = blotImage(resultImage, wp.Left(), offset) + } + + // The top edge. + for x := 0; x < width; x += size.W { + offset := image.Pt(x, 0) + resultImage = blotImage(resultImage, wp.Top(), offset) + } + + // The top left corner. + resultImage = blotImage(resultImage, wp.Corner(), image.Point{}) + } + + return resultImage +} + +func blotImage(target, source *image.RGBA, offset image.Point) *image.RGBA { + b := target.Bounds() + newImg := image.NewRGBA(b) + draw.Draw(newImg, b, target, image.Point{}, draw.Src) + draw.Draw(newImg, source.Bounds().Add(offset), source, image.Point{}, draw.Over) + return newImg +} diff --git a/pkg/userdir/userdir.go b/pkg/userdir/userdir.go index 2a6ed6b..a559906 100644 --- a/pkg/userdir/userdir.go +++ b/pkg/userdir/userdir.go @@ -15,10 +15,11 @@ import ( var ( ConfigDirectoryName = "doodle" - ProfileDirectory string - LevelDirectory string - DoodadDirectory string - CampaignDirectory string + ProfileDirectory string + LevelDirectory string + DoodadDirectory string + CampaignDirectory string + ScreenshotDirectory string CacheDirectory string FontDirectory string @@ -36,6 +37,7 @@ func init() { LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels") DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads") CampaignDirectory = configdir.LocalConfig(ConfigDirectoryName, "campaigns") + ScreenshotDirectory = configdir.LocalConfig(ConfigDirectoryName, "screenshots") // Cache directory to extract font files to. CacheDirectory = configdir.LocalCache(ConfigDirectoryName) @@ -48,6 +50,7 @@ func init() { configdir.MakePath(DoodadDirectory) configdir.MakePath(CampaignDirectory) configdir.MakePath(FontDirectory) + configdir.MakePath(ScreenshotDirectory) } } diff --git a/pkg/wallpaper/wallpaper.go b/pkg/wallpaper/wallpaper.go index eb58c15..76a7560 100644 --- a/pkg/wallpaper/wallpaper.go +++ b/pkg/wallpaper/wallpaper.go @@ -156,7 +156,6 @@ func (wp *Wallpaper) cache() { wp.top = slice(wp.quarterWidth, 0) wp.left = slice(0, wp.quarterHeight) wp.repeat = slice(wp.quarterWidth, wp.quarterHeight) - } // QuarterSize returns the width and height of the quarter images.