Giant Screenshot Feature
In the Level Editor, the "Level->Giant Screenshot" menu will take a full scale PNG screenshot of the entire level, with its wallpaper and doodads, and save it in ~/.config/doodle/screenshots. It is currently CPU intensive and slow. With future work it should be made asynchronous. The function is abstracted away nicely so that the doodad CLI tool may support this as well.
This commit is contained in:
parent
55efdd6eb5
commit
4469847c72
|
@ -8,8 +8,10 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
"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/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/native"
|
"git.kirsle.net/apps/doodle/pkg/native"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/pkg/windows"
|
"git.kirsle.net/apps/doodle/pkg/windows"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
|
@ -121,6 +123,20 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
||||||
levelMenu.AddItemAccel("Playtest", "P", func() {
|
levelMenu.AddItemAccel("Playtest", "P", func() {
|
||||||
u.Scene.Playtest()
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
////////
|
////////
|
||||||
|
|
|
@ -131,34 +131,12 @@ func (c *Chunker) IterPixels() <-chan Pixel {
|
||||||
// manage: the lowest pixels from the lowest chunks to the highest pixels of
|
// manage: the lowest pixels from the lowest chunks to the highest pixels of
|
||||||
// the highest chunks.
|
// the highest chunks.
|
||||||
func (c *Chunker) WorldSize() render.Rect {
|
func (c *Chunker) WorldSize() render.Rect {
|
||||||
// Lowest and highest chunks.
|
chunkLowest, chunkHighest := c.Bounds()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return render.Rect{
|
return render.Rect{
|
||||||
X: chunkLowest.X * size,
|
X: chunkLowest.X * c.Size,
|
||||||
Y: chunkLowest.Y * size,
|
Y: chunkLowest.Y * c.Size,
|
||||||
W: (chunkHighest.X * size) + (size - 1),
|
W: (chunkHighest.X * c.Size) + (c.Size - 1),
|
||||||
H: (chunkHighest.Y * size) + (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.
|
// 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]
|
||||||
|
|
180
pkg/level/giant_screenshot/giant_screenshot.go
Normal file
180
pkg/level/giant_screenshot/giant_screenshot.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ var (
|
||||||
LevelDirectory string
|
LevelDirectory string
|
||||||
DoodadDirectory string
|
DoodadDirectory string
|
||||||
CampaignDirectory string
|
CampaignDirectory string
|
||||||
|
ScreenshotDirectory string
|
||||||
|
|
||||||
CacheDirectory string
|
CacheDirectory string
|
||||||
FontDirectory string
|
FontDirectory string
|
||||||
|
@ -36,6 +37,7 @@ func init() {
|
||||||
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
|
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
|
||||||
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
|
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
|
||||||
CampaignDirectory = configdir.LocalConfig(ConfigDirectoryName, "campaigns")
|
CampaignDirectory = configdir.LocalConfig(ConfigDirectoryName, "campaigns")
|
||||||
|
ScreenshotDirectory = configdir.LocalConfig(ConfigDirectoryName, "screenshots")
|
||||||
|
|
||||||
// Cache directory to extract font files to.
|
// Cache directory to extract font files to.
|
||||||
CacheDirectory = configdir.LocalCache(ConfigDirectoryName)
|
CacheDirectory = configdir.LocalCache(ConfigDirectoryName)
|
||||||
|
@ -48,6 +50,7 @@ func init() {
|
||||||
configdir.MakePath(DoodadDirectory)
|
configdir.MakePath(DoodadDirectory)
|
||||||
configdir.MakePath(CampaignDirectory)
|
configdir.MakePath(CampaignDirectory)
|
||||||
configdir.MakePath(FontDirectory)
|
configdir.MakePath(FontDirectory)
|
||||||
|
configdir.MakePath(ScreenshotDirectory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,6 @@ func (wp *Wallpaper) cache() {
|
||||||
wp.top = slice(wp.quarterWidth, 0)
|
wp.top = slice(wp.quarterWidth, 0)
|
||||||
wp.left = slice(0, wp.quarterHeight)
|
wp.left = slice(0, wp.quarterHeight)
|
||||||
wp.repeat = slice(wp.quarterWidth, wp.quarterHeight)
|
wp.repeat = slice(wp.quarterWidth, wp.quarterHeight)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuarterSize returns the width and height of the quarter images.
|
// QuarterSize returns the width and height of the quarter images.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user