2021-10-04 00:21:17 +00:00
|
|
|
package giant_screenshot
|
|
|
|
|
|
|
|
import (
|
2021-10-08 01:24:18 +00:00
|
|
|
"errors"
|
2021-10-04 00:21:17 +00:00
|
|
|
"image"
|
|
|
|
"image/draw"
|
|
|
|
"image/png"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"time"
|
|
|
|
|
2022-09-24 22:17:25 +00:00
|
|
|
"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/shmem"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/wallpaper"
|
2021-10-04 00:21:17 +00:00
|
|
|
"git.kirsle.net/go/render"
|
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
Giant Screenshot functionality for the Level Editor.
|
|
|
|
*/
|
|
|
|
|
2021-10-08 01:24:18 +00:00
|
|
|
var locked bool
|
|
|
|
|
2021-10-04 00:21:17 +00:00
|
|
|
// GiantScreenshot returns a rendered RGBA image of the entire level.
|
2021-10-08 01:24:18 +00:00
|
|
|
//
|
|
|
|
// Only one thread should be doing this at a time. A sync.Mutex will cause
|
|
|
|
// an error to return if another goroutine is already in the process of
|
|
|
|
// generating a screenshot, and you'll have to wait and try later.
|
|
|
|
func GiantScreenshot(lvl *level.Level) (image.Image, error) {
|
|
|
|
// Lock this to one user at a time.
|
|
|
|
if locked {
|
|
|
|
return nil, errors.New("a giant screenshot is still being processed; try later...")
|
|
|
|
}
|
|
|
|
locked = true
|
|
|
|
defer func() {
|
|
|
|
locked = false
|
|
|
|
}()
|
|
|
|
|
|
|
|
shmem.Flash("Saving a giant screenshot (this takes a moment)...")
|
|
|
|
|
2021-10-04 00:21:17 +00:00
|
|
|
// How big will our image be?
|
|
|
|
var (
|
|
|
|
size = lvl.Chunker.WorldSizePositive()
|
2023-02-17 05:47:18 +00:00
|
|
|
chunkSize = int(lvl.Chunker.Size)
|
2021-10-04 00:21:17 +00:00
|
|
|
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)
|
2023-12-09 03:48:02 +00:00
|
|
|
img = WallpaperToImage(lvl, img, size.W, size.H, render.Origin)
|
2021-10-04 00:21:17 +00:00
|
|
|
|
|
|
|
// 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 {
|
2023-02-19 01:37:54 +00:00
|
|
|
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, lvl, false)
|
2021-10-04 00:21:17 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Error("GiantScreenshot: Load doodad: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Offset the doodad position if the image is displaying
|
|
|
|
// negative coordinates.
|
2021-10-10 03:45:38 +00:00
|
|
|
drawAt := render.NewPoint(actor.Point.X, actor.Point.Y)
|
2021-10-04 00:21:17 +00:00
|
|
|
if worldSize.X < 0 {
|
2021-10-10 03:45:38 +00:00
|
|
|
var offset = render.AbsInt(worldSize.X) * chunkSize
|
|
|
|
drawAt.X += offset
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
if worldSize.Y < 0 {
|
2021-10-10 03:45:38 +00:00
|
|
|
var offset = render.AbsInt(worldSize.Y) * chunkSize
|
|
|
|
drawAt.Y += offset
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
}
|
2021-10-10 03:45:38 +00:00
|
|
|
img = blotImage(img, rgba, image.Pt(drawAt.X, drawAt.Y))
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-10-08 01:24:18 +00:00
|
|
|
return img, nil
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
2021-10-08 01:24:18 +00:00
|
|
|
img, err := GiantScreenshot(level)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-10-04 00:21:17 +00:00
|
|
|
|
|
|
|
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.
|
2023-12-09 03:48:02 +00:00
|
|
|
//
|
|
|
|
// 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 {
|
2021-10-04 00:21:17 +00:00
|
|
|
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
|
|
|
|
|
2023-12-09 03:48:02 +00:00
|
|
|
// Handling the scroll offsets: wallpapers are made up of small-ish
|
|
|
|
// repeating squares so the scroll offset needs only be a modulus within
|
|
|
|
// the size of one square.
|
|
|
|
absOffset := 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)
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tile the left edge for bounded lvls.
|
|
|
|
if lvl.PageType > level.Unbounded {
|
2023-12-09 03:48:02 +00:00
|
|
|
// The left edge (unless off screen)
|
|
|
|
if absOffset.X < size.W {
|
|
|
|
for y := 0; y < height; y += size.H {
|
|
|
|
dst := image.Pt(0-offset.X, y-offset.Y)
|
|
|
|
resultImage = blotImage(resultImage, wp.Left(), dst)
|
|
|
|
}
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The top edge.
|
2023-12-09 03:48:02 +00:00
|
|
|
if absOffset.Y < size.H {
|
|
|
|
for x := 0; x < width; x += size.W {
|
|
|
|
dst := image.Pt(x-offset.X, 0-offset.Y)
|
|
|
|
resultImage = blotImage(resultImage, wp.Top(), dst)
|
|
|
|
}
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The top left corner.
|
2023-12-09 03:48:02 +00:00
|
|
|
if absOffset.X < size.W && absOffset.Y < size.H {
|
|
|
|
resultImage = blotImage(resultImage, wp.Corner(), image.Pt(
|
|
|
|
-offset.X,
|
|
|
|
-offset.Y,
|
|
|
|
))
|
|
|
|
}
|
2021-10-04 00:21:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|