Level Screenshots and Thumbnails
Adds some support for "less giant" level screenshots. * In the Editor, the Level->Take Screenshot menu will render a cropped screen shot of just the level viewport on screen. Note: it is not an SDL2 screen copy but generated from scratch from the level data. * In levels themselves, screenshots can be stored inside the level data in three different sizes: large (1280x720), medium and small (each a halved size of the previous). * The first screenshot is created when the level is saved, starting from wherever the scroll position in the editor is at, and recording the 720p view of the level from there. * The level screenshot can be previewed and updated in the Level Properties window of the editor: so you can scroll the editor to just the right position and take a good screenshot to represent your level. * In the future: these embedded level screenshots will be displayed on the Story Mode and other screens to see a preview of each level. Other tweaks: * When taking a Giant Screenshot: a confirm modal will warn the player that it may take a while. And during the screenshot, show the new Wait Modal to block player interaction until the screenshot has finished.
This commit is contained in:
parent
481638bea6
commit
da83231559
|
@ -153,6 +153,14 @@ var (
|
||||||
// variable is the tolerance offset - if they are only this far out of bounds, put them
|
// variable is the tolerance offset - if they are only this far out of bounds, put them
|
||||||
// back in bounds but further out and they're OK.
|
// back in bounds but further out and they're OK.
|
||||||
OutOfBoundsMargin = 40
|
OutOfBoundsMargin = 40
|
||||||
|
|
||||||
|
// Level screenshot dimensions saved within the level data.
|
||||||
|
LevelScreenshotLargeFilename = "large.png"
|
||||||
|
LevelScreenshotMediumFilename = "medium.png"
|
||||||
|
LevelScreenshotSmallFilename = "small.png"
|
||||||
|
LevelScreenshotLargeSize = render.NewRect(1280, 720)
|
||||||
|
LevelScreenshotMediumSize = render.NewRect(640, 360)
|
||||||
|
LevelScreenshotSmallSize = render.NewRect(320, 180)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Edit Mode Values
|
// Edit Mode Values
|
||||||
|
|
|
@ -141,6 +141,13 @@ var (
|
||||||
Color: render.Black,
|
Color: render.Black,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DangerFont is a red version of UIFont.
|
||||||
|
DangerFont = render.Text{
|
||||||
|
Size: 12,
|
||||||
|
Padding: 4,
|
||||||
|
Color: render.Red,
|
||||||
|
}
|
||||||
|
|
||||||
// LabelFont is the font for strong labels in UI.
|
// LabelFont is the font for strong labels in UI.
|
||||||
LabelFont = render.Text{
|
LabelFont = render.Text{
|
||||||
Size: 12,
|
Size: 12,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level/giant_screenshot"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/level/publishing"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level/publishing"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
|
@ -553,9 +554,28 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.lastAutosaveAt = time.Now()
|
s.lastAutosaveAt = time.Now()
|
||||||
|
|
||||||
|
// Save screenshots into the level file.
|
||||||
|
if !m.HasScreenshot() {
|
||||||
|
s.UpdateLevelScreenshot(m)
|
||||||
|
}
|
||||||
|
|
||||||
return m.WriteFile(filename)
|
return m.WriteFile(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateLevelScreenshot updates a level screenshot in its zipfile.
|
||||||
|
func (s *EditorScene) UpdateLevelScreenshot(lvl *level.Level) error {
|
||||||
|
// The level must have been saved and have a filename to update in.
|
||||||
|
if s.filename == "" {
|
||||||
|
return errors.New("Save your level to disk before updating its screenshot.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := giant_screenshot.UpdateLevelScreenshots(lvl, s.UI.Canvas.Viewport().Point()); err != nil {
|
||||||
|
return fmt.Errorf("Error saving level screenshots: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AutoSave takes an autosave snapshot of the level or drawing.
|
// AutoSave takes an autosave snapshot of the level or drawing.
|
||||||
func (s *EditorScene) AutoSave() error {
|
func (s *EditorScene) AutoSave() error {
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/level/giant_screenshot"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level/giant_screenshot"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/windows"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/windows"
|
||||||
|
@ -115,19 +116,41 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
||||||
})
|
})
|
||||||
|
|
||||||
levelMenu.AddSeparator()
|
levelMenu.AddSeparator()
|
||||||
levelMenu.AddItem("Giant Screenshot", func() {
|
levelMenu.AddItem("Screenshot", func() {
|
||||||
// It takes a LONG TIME to render for medium+ maps.
|
// It takes a LONG TIME to render for medium+ maps.
|
||||||
// Do so on a background thread.
|
// Do so on a background thread.
|
||||||
go func() {
|
go func() {
|
||||||
filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level)
|
filename, err := giant_screenshot.SaveCroppedScreenshot(u.Scene.Level, u.Scene.GetDrawing().Viewport())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.FlashError("Error: %s", err.Error())
|
d.FlashError("Error: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
d.FlashError("Giant screenshot saved as: %s", filename)
|
d.FlashError("Screenshot saved as: %s", filename)
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
levelMenu.AddItem("Giant Screenshot", func() {
|
||||||
|
// It takes a LONG TIME to render for medium+ maps.
|
||||||
|
modal.Confirm(
|
||||||
|
"Do you want to make a 'Giant Screenshot' of\n" +
|
||||||
|
"your WHOLE level? Note: this may take several\n" +
|
||||||
|
"seconds for very large maps!",
|
||||||
|
).WithTitle("Giant Screenshot").Then(func() {
|
||||||
|
// Show the wait modal and generate the screenshot on a background thread.
|
||||||
|
m := modal.Wait("Generating a giant screenshot...").WithTitle("Please hold")
|
||||||
|
go func() {
|
||||||
|
defer m.Dismiss(true)
|
||||||
|
|
||||||
|
filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level)
|
||||||
|
if err != nil {
|
||||||
|
d.FlashError("Error: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.FlashError("Giant screenshot saved as: %s", filename)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
})
|
||||||
levelMenu.AddItem("Open screenshot folder", func() {
|
levelMenu.AddItem("Open screenshot folder", func() {
|
||||||
native.OpenLocalURL(userdir.ScreenshotDirectory)
|
native.OpenLocalURL(userdir.ScreenshotDirectory)
|
||||||
})
|
})
|
||||||
|
|
|
@ -188,6 +188,9 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
u.Canvas.Destroy() // clean up old textures
|
u.Canvas.Destroy() // clean up old textures
|
||||||
u.Canvas.LoadLevel(scene.Level)
|
u.Canvas.LoadLevel(scene.Level)
|
||||||
},
|
},
|
||||||
|
OnUpdateScreenshot: func() error {
|
||||||
|
return scene.UpdateLevelScreenshot(scene.Level)
|
||||||
|
},
|
||||||
OnReload: func() {
|
OnReload: func() {
|
||||||
log.Warn("RELOAD LEVEL")
|
log.Warn("RELOAD LEVEL")
|
||||||
scene.Reset()
|
scene.Reset()
|
||||||
|
|
|
@ -37,6 +37,28 @@ func NewFileSystem() *FileSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exists checks if a file exists.
|
||||||
|
func (fs *FileSystem) Exists(filename string) bool {
|
||||||
|
if fs.filemap == nil {
|
||||||
|
fs.filemap = map[string]File{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy file map.
|
||||||
|
if _, ok := fs.filemap[filename]; ok {
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check in the zipfile.
|
||||||
|
if fs.Zipfile != nil {
|
||||||
|
file, err := fs.Zipfile.Open(filename)
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Get a file from the FileSystem.
|
// Get a file from the FileSystem.
|
||||||
func (fs *FileSystem) Get(filename string) ([]byte, error) {
|
func (fs *FileSystem) Get(filename string) ([]byte, error) {
|
||||||
if fs.filemap == nil {
|
if fs.filemap == nil {
|
||||||
|
|
|
@ -72,7 +72,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) {
|
||||||
|
|
||||||
// Render the wallpaper onto it.
|
// Render the wallpaper onto it.
|
||||||
log.Debug("GiantScreenshot: Render wallpaper to image (%s)...", size)
|
log.Debug("GiantScreenshot: Render wallpaper to image (%s)...", size)
|
||||||
img = WallpaperToImage(lvl, img, size.W, size.H)
|
img = WallpaperToImage(lvl, img, size.W, size.H, render.Origin)
|
||||||
|
|
||||||
// Render the chunks.
|
// Render the chunks.
|
||||||
log.Debug("GiantScreenshot: Render level chunks...")
|
log.Debug("GiantScreenshot: Render level chunks...")
|
||||||
|
@ -158,7 +158,9 @@ func SaveGiantScreenshot(level *level.Level) (string, error) {
|
||||||
//
|
//
|
||||||
// The image is assumed to have a rect of (0,0,width,height) and that
|
// 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.
|
// width and height are positive. Used for the Giant Screenshot feature.
|
||||||
func WallpaperToImage(lvl *level.Level, target *image.RGBA, width, height int) *image.RGBA {
|
//
|
||||||
|
// Pass an offset point to 'scroll' the wallpaper (for the Cropped Screenshot).
|
||||||
|
func WallpaperToImage(lvl *level.Level, target *image.RGBA, width, height int, offset render.Point) *image.RGBA {
|
||||||
wp, err := wallpaper.FromFile("assets/wallpapers/"+lvl.Wallpaper, lvl)
|
wp, err := wallpaper.FromFile("assets/wallpapers/"+lvl.Wallpaper, lvl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GiantScreenshot: wallpaper load: %s", err)
|
log.Error("GiantScreenshot: wallpaper load: %s", err)
|
||||||
|
@ -167,30 +169,48 @@ func WallpaperToImage(lvl *level.Level, target *image.RGBA, width, height int) *
|
||||||
var size = wp.QuarterRect()
|
var size = wp.QuarterRect()
|
||||||
var resultImage = target
|
var resultImage = target
|
||||||
|
|
||||||
// Tile the repeat texture.
|
// Handling the scroll offsets: wallpapers are made up of small-ish
|
||||||
for x := 0; x < width; x += size.W {
|
// repeating squares so the scroll offset needs only be a modulus within
|
||||||
for y := 0; y < height; y += size.H {
|
// the size of one square.
|
||||||
offset := image.Pt(x, y)
|
absOffset := offset
|
||||||
resultImage = blotImage(resultImage, wp.Repeat(), offset)
|
if offset != render.Origin {
|
||||||
|
offset.X = render.AbsInt(offset.X % size.W)
|
||||||
|
offset.Y = render.AbsInt(offset.Y % size.H)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile the repeat texture. Go one tile extra in case of offset.
|
||||||
|
for x := 0; x < width+size.W; x += size.W {
|
||||||
|
for y := 0; y < height+size.H; y += size.H {
|
||||||
|
dst := image.Pt(x-offset.X, y-offset.Y)
|
||||||
|
resultImage = blotImage(resultImage, wp.Repeat(), dst)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tile the left edge for bounded lvls.
|
// Tile the left edge for bounded lvls.
|
||||||
if lvl.PageType > level.Unbounded {
|
if lvl.PageType > level.Unbounded {
|
||||||
// The left edge.
|
// The left edge (unless off screen)
|
||||||
for y := 0; y < height; y += size.H {
|
if absOffset.X < size.W {
|
||||||
offset := image.Pt(0, y)
|
for y := 0; y < height; y += size.H {
|
||||||
resultImage = blotImage(resultImage, wp.Left(), offset)
|
dst := image.Pt(0-offset.X, y-offset.Y)
|
||||||
|
resultImage = blotImage(resultImage, wp.Left(), dst)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The top edge.
|
// The top edge.
|
||||||
for x := 0; x < width; x += size.W {
|
if absOffset.Y < size.H {
|
||||||
offset := image.Pt(x, 0)
|
for x := 0; x < width; x += size.W {
|
||||||
resultImage = blotImage(resultImage, wp.Top(), offset)
|
dst := image.Pt(x-offset.X, 0-offset.Y)
|
||||||
|
resultImage = blotImage(resultImage, wp.Top(), dst)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The top left corner.
|
// The top left corner.
|
||||||
resultImage = blotImage(resultImage, wp.Corner(), image.Point{})
|
if absOffset.X < size.W && absOffset.Y < size.H {
|
||||||
|
resultImage = blotImage(resultImage, wp.Corner(), image.Pt(
|
||||||
|
-offset.X,
|
||||||
|
-offset.Y,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultImage
|
return resultImage
|
||||||
|
|
196
pkg/level/giant_screenshot/regular_screenshot.go
Normal file
196
pkg/level/giant_screenshot/regular_screenshot.go
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
package giant_screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CroppedScreenshot returns a rendered RGBA image of the level.
|
||||||
|
func CroppedScreenshot(lvl *level.Level, viewport render.Rect) (image.Image, error) {
|
||||||
|
// Lock this to one user at a time.
|
||||||
|
if locked {
|
||||||
|
return nil, errors.New("a screenshot is still being processed; try later...")
|
||||||
|
}
|
||||||
|
locked = true
|
||||||
|
defer func() {
|
||||||
|
locked = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
// How big will our image be?
|
||||||
|
var (
|
||||||
|
size = render.NewRect(viewport.W-viewport.X, viewport.H-viewport.Y)
|
||||||
|
chunkSize = int(lvl.Chunker.Size)
|
||||||
|
// worldSize = viewport
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the image.
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, size.W, size.H))
|
||||||
|
|
||||||
|
// Render the wallpaper onto it.
|
||||||
|
log.Debug("CroppedScreenshot: Render wallpaper to image (%s)...", size)
|
||||||
|
img = WallpaperToImage(lvl, img, size.W, size.H, viewport.Point())
|
||||||
|
|
||||||
|
// Render the chunks.
|
||||||
|
log.Debug("CroppedScreenshot: Render level chunks...")
|
||||||
|
for coord := range lvl.Chunker.IterViewportChunks(viewport) {
|
||||||
|
if chunk, ok := lvl.Chunker.GetChunk(coord); ok {
|
||||||
|
|
||||||
|
// Get this chunk's rendered bitmap.
|
||||||
|
rgba, ok := chunk.CachedBitmap(render.Invisible).(*image.RGBA)
|
||||||
|
if !ok {
|
||||||
|
log.Error("CroppedScreenshot: couldn't turn chunk to RGBA")
|
||||||
|
}
|
||||||
|
log.Debug("Blot chunk %s onto image", coord)
|
||||||
|
|
||||||
|
// Compute where on the output image to copy this bitmap to.
|
||||||
|
dst := image.Pt(
|
||||||
|
// (X * W) multiplies the chunk coord by its size,
|
||||||
|
// Then subtract the Viewport (level scroll position)
|
||||||
|
(coord.X*chunkSize)-viewport.X,
|
||||||
|
(coord.Y*chunkSize)-viewport.Y,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debug("Copy chunk: %s to %s", coord, dst)
|
||||||
|
img = blotImage(img, rgba, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the doodads.
|
||||||
|
log.Debug("CroppedScreenshot: Render actors...")
|
||||||
|
for _, actor := range lvl.Actors {
|
||||||
|
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("CroppedScreenshot: Load doodad: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset the doodad position by the viewport (scroll position).
|
||||||
|
drawAt := render.NewPoint(actor.Point.X, actor.Point.Y)
|
||||||
|
drawAt.X -= viewport.X
|
||||||
|
drawAt.Y -= viewport.Y
|
||||||
|
|
||||||
|
// 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("CroppedScreenshot: couldn't turn chunk to RGBA")
|
||||||
|
}
|
||||||
|
img = blotImage(img, rgba, image.Pt(drawAt.X, drawAt.Y))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCroppedScreenshot will take a screenshot and write it to a file on disk,
|
||||||
|
// returning the filename relative to ~/.config/doodle/screenshots
|
||||||
|
func SaveCroppedScreenshot(level *level.Level, viewport render.Rect) (string, error) {
|
||||||
|
var filename = time.Now().Format("2006-01-02_15-04-05.png")
|
||||||
|
|
||||||
|
img, err := CroppedScreenshot(level, viewport)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
fh, err := os.Create(filepath.Join(userdir.ScreenshotDirectory, filename))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
png.Encode(fh, img)
|
||||||
|
return filename, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLevelScreenshots will generate and embed the screenshot PNGs into the level data.
|
||||||
|
func UpdateLevelScreenshots(lvl *level.Level, scroll render.Point) error {
|
||||||
|
// Take screenshots.
|
||||||
|
large, medium, small, err := CreateLevelScreenshots(lvl, scroll)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the images into the level's filesystem.
|
||||||
|
for filename, img := range map[string]image.Image{
|
||||||
|
balance.LevelScreenshotLargeFilename: large,
|
||||||
|
balance.LevelScreenshotMediumFilename: medium,
|
||||||
|
balance.LevelScreenshotSmallFilename: small,
|
||||||
|
} {
|
||||||
|
var fh = bytes.NewBuffer([]byte{})
|
||||||
|
if err := png.Encode(fh, img); err != nil {
|
||||||
|
return fmt.Errorf("encode %s: %s", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("UpdateLevelScreenshots: add %s", filename)
|
||||||
|
lvl.Files.Set(
|
||||||
|
fmt.Sprintf("assets/screenshots/%s", filename),
|
||||||
|
fh.Bytes(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLevelScreenshots generates a screenshot to save with the level data.
|
||||||
|
//
|
||||||
|
// This is called by the editor upon level save, and outputs the screenshots that
|
||||||
|
// will be embedded within the level data itself.
|
||||||
|
//
|
||||||
|
// Returns the large, medium and small images.
|
||||||
|
func CreateLevelScreenshots(lvl *level.Level, scroll render.Point) (large, medium, small image.Image, err error) {
|
||||||
|
// Viewport to screenshot.
|
||||||
|
viewport := render.Rect{
|
||||||
|
X: scroll.X,
|
||||||
|
W: scroll.X + balance.LevelScreenshotLargeSize.W,
|
||||||
|
Y: scroll.Y,
|
||||||
|
H: scroll.Y + balance.LevelScreenshotLargeSize.H,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the full size screenshot as an image.
|
||||||
|
large, err = CroppedScreenshot(lvl, viewport)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the medium and small versions.
|
||||||
|
medium = Scale(large, image.Rect(0, 0, balance.LevelScreenshotMediumSize.W, balance.LevelScreenshotMediumSize.H), draw.ApproxBiLinear)
|
||||||
|
small = Scale(large, image.Rect(0, 0, balance.LevelScreenshotSmallSize.W, balance.LevelScreenshotSmallSize.H), draw.ApproxBiLinear)
|
||||||
|
return large, medium, small, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale down an image. Example:
|
||||||
|
//
|
||||||
|
// scaled := Scale(src, image.Rect(0, 0, 200, 200), draw.ApproxBiLinear)
|
||||||
|
func Scale(src image.Image, rect image.Rectangle, scale draw.Scaler) image.Image {
|
||||||
|
dst := image.NewRGBA(rect)
|
||||||
|
copyRect := image.Rect(
|
||||||
|
rect.Min.X,
|
||||||
|
rect.Min.Y,
|
||||||
|
rect.Min.X+rect.Max.X,
|
||||||
|
rect.Min.Y+rect.Max.Y,
|
||||||
|
)
|
||||||
|
scale.Scale(dst, copyRect, src, src.Bounds(), draw.Over, nil)
|
||||||
|
return dst
|
||||||
|
}
|
61
pkg/level/screenshot.go
Normal file
61
pkg/level/screenshot.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
|
||||||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper functions to get a level's embedded screenshot PNGs as textures.
|
||||||
|
|
||||||
|
// HasScreenshot returns whether screenshots exist for the level.
|
||||||
|
func (lvl *Level) HasScreenshot() bool {
|
||||||
|
var filenames = []string{
|
||||||
|
balance.LevelScreenshotLargeFilename,
|
||||||
|
balance.LevelScreenshotMediumFilename,
|
||||||
|
balance.LevelScreenshotSmallFilename,
|
||||||
|
}
|
||||||
|
for _, filename := range filenames {
|
||||||
|
if lvl.Files.Exists("assets/screenshots/" + filename) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScreenshotImage returns a screenshot from the level's data as a Go Image.
|
||||||
|
// The filename is like "large.png" or "medium.png" and is appended to "assets/screenshots"
|
||||||
|
func (lvl *Level) GetScreenshotImage(filename string) (image.Image, error) {
|
||||||
|
data, err := lvl.Files.Get("assets/screenshots/" + filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return png.Decode(bytes.NewBuffer(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScreenshotImageAsUIImage returns a ui.Image texture of a screenshot.
|
||||||
|
func (lvl *Level) GetScreenshotImageAsUIImage(filename string) (*ui.Image, error) {
|
||||||
|
// Have it cached recently?
|
||||||
|
if lvl.cacheImages == nil {
|
||||||
|
lvl.cacheImages = map[string]*ui.Image{}
|
||||||
|
} else if img, ok := lvl.cacheImages[filename]; ok {
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := lvl.GetScreenshotImage(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ui.ImageFromImage(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lvl.cacheImages[filename] = result
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||||||
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Useful variables.
|
// Useful variables.
|
||||||
|
@ -73,6 +74,9 @@ type Level struct {
|
||||||
|
|
||||||
// Undo history, temporary live data not persisted to the level file.
|
// Undo history, temporary live data not persisted to the level file.
|
||||||
UndoHistory *drawtool.History `json:"-"`
|
UndoHistory *drawtool.History `json:"-"`
|
||||||
|
|
||||||
|
// Cache of loaded images (e.g. screenshots).
|
||||||
|
cacheImages map[string]*ui.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
// GameRule
|
// GameRule
|
||||||
|
@ -118,6 +122,13 @@ func (m *Level) Teardown() {
|
||||||
textures += freed
|
textures += freed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Free any cached images (screenshots)
|
||||||
|
if m.cacheImages != nil {
|
||||||
|
for _, img := range m.cacheImages {
|
||||||
|
img.Destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug("Teardown level (%s): Freed %d textures across %d level chunks", m.Title, textures, chunks)
|
log.Debug("Teardown level (%s): Freed %d textures across %d level chunks", m.Title, textures, chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ type AddEditLevel struct {
|
||||||
OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string)
|
OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string)
|
||||||
OnCreateNewLevel func(*level.Level)
|
OnCreateNewLevel func(*level.Level)
|
||||||
OnCreateNewDoodad func(width, height int)
|
OnCreateNewDoodad func(width, height int)
|
||||||
|
OnUpdateScreenshot func() error
|
||||||
OnReload func()
|
OnReload func()
|
||||||
OnCancel func()
|
OnCancel func()
|
||||||
}
|
}
|
||||||
|
@ -74,6 +75,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
|
||||||
} else {
|
} else {
|
||||||
// Additional Level tabs (existing level only)
|
// Additional Level tabs (existing level only)
|
||||||
config.setupGameRuleFrame(tabframe)
|
config.setupGameRuleFrame(tabframe)
|
||||||
|
config.setupScreenshotFrame(tabframe)
|
||||||
config.setupAdvancedFrame(tabframe)
|
config.setupAdvancedFrame(tabframe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,6 +566,91 @@ func (config AddEditLevel) setupGameRuleFrame(tf *ui.TabFrame) {
|
||||||
form.Create(frame, fields)
|
form.Create(frame, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Level Screenshot management frame.
|
||||||
|
func (config AddEditLevel) setupScreenshotFrame(tf *ui.TabFrame) {
|
||||||
|
frame := tf.AddTab("Screenshot", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Screenshot",
|
||||||
|
Font: balance.TabFont,
|
||||||
|
}))
|
||||||
|
|
||||||
|
var image *ui.Image
|
||||||
|
|
||||||
|
// Have a screenshot already?
|
||||||
|
if config.EditLevel.HasScreenshot() {
|
||||||
|
if img, err := config.EditLevel.GetScreenshotImageAsUIImage(balance.LevelScreenshotSmallFilename); err != nil {
|
||||||
|
lbl := ui.NewLabel(ui.Label{
|
||||||
|
Text: err.Error(),
|
||||||
|
Font: balance.DangerFont,
|
||||||
|
})
|
||||||
|
frame.Pack(lbl, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Draw the image.
|
||||||
|
log.Error("Got img: %+v", img)
|
||||||
|
frame.Pack(img, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hold onto it in case we need to refresh it.
|
||||||
|
image = img
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lbl := ui.NewLabel(ui.Label{
|
||||||
|
Text: "This level has no screenshot available. If this is\n" +
|
||||||
|
"a new drawing, its first screenshot will be created\n" +
|
||||||
|
"upon save; playtest or close and re-open the level\n" +
|
||||||
|
"then and its screenshot should appear here and can\n" +
|
||||||
|
"be refreshed.",
|
||||||
|
Font: balance.DangerFont,
|
||||||
|
})
|
||||||
|
frame.Pack(lbl, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Exit now - don't add the Refresh button in case of nil pointer exception
|
||||||
|
// while we are in this state.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image refresh button.
|
||||||
|
btn := ui.NewButton("Refresh", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Update Screenshot",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
}))
|
||||||
|
ui.NewTooltip(btn, ui.Tooltip{
|
||||||
|
Text: "Update the screenshot from your current scroll point\nin the level editor.",
|
||||||
|
})
|
||||||
|
|
||||||
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
// Take a new screenshot.
|
||||||
|
if config.OnUpdateScreenshot == nil {
|
||||||
|
shmem.FlashError("OnUpdateScreenshot handler not configured!")
|
||||||
|
} else {
|
||||||
|
if err := config.OnUpdateScreenshot(); err != nil {
|
||||||
|
modal.Alert(err.Error()).WithTitle("Error updating screenshot")
|
||||||
|
} else {
|
||||||
|
// Get the updated screenshot image from the level.
|
||||||
|
if img, err := config.EditLevel.GetScreenshotImage(balance.LevelScreenshotSmallFilename); err != nil {
|
||||||
|
shmem.FlashError("Couldn't reload screenshot from level: %s", err)
|
||||||
|
} else {
|
||||||
|
image.ReplaceFromImage(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
btn.Compute(config.Engine)
|
||||||
|
|
||||||
|
frame.Pack(btn, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
config.Supervisor.Add(btn)
|
||||||
|
}
|
||||||
|
|
||||||
// Creates the Game Rules frame for existing level (set difficulty, etc.)
|
// Creates the Game Rules frame for existing level (set difficulty, etc.)
|
||||||
func (config AddEditLevel) setupAdvancedFrame(tf *ui.TabFrame) {
|
func (config AddEditLevel) setupAdvancedFrame(tf *ui.TabFrame) {
|
||||||
frame := tf.AddTab("Advanced", ui.NewLabel(ui.Label{
|
frame := tf.AddTab("Advanced", ui.NewLabel(ui.Label{
|
||||||
|
|
Loading…
Reference in New Issue
Block a user