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.pull/84/head
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]
|
||||||
|
|
|
@ -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