2023-12-09 03:48:02 +00:00
|
|
|
package giant_screenshot
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"image"
|
|
|
|
"image/png"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-12-09 05:52:34 +00:00
|
|
|
"runtime"
|
2023-12-09 03:48:02 +00:00
|
|
|
"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) {
|
2023-12-09 05:52:34 +00:00
|
|
|
// Not for WASM for now.
|
|
|
|
if runtime.GOOS == "js" {
|
|
|
|
return nil, errors.New("screenshots not yet supported for WASM")
|
|
|
|
}
|
|
|
|
|
2023-12-09 03:48:02 +00:00
|
|
|
// 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.
|
2023-12-09 22:59:31 +00:00
|
|
|
large, medium, small, tiny, err := CreateLevelScreenshots(lvl, scroll)
|
2023-12-09 03:48:02 +00:00
|
|
|
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,
|
2023-12-09 22:59:31 +00:00
|
|
|
balance.LevelScreenshotTinyFilename: tiny,
|
2023-12-09 03:48:02 +00:00
|
|
|
} {
|
|
|
|
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.
|
2023-12-09 22:59:31 +00:00
|
|
|
func CreateLevelScreenshots(lvl *level.Level, scroll render.Point) (large, medium, small, tiny image.Image, err error) {
|
2023-12-09 03:48:02 +00:00
|
|
|
// 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)
|
2023-12-09 22:59:31 +00:00
|
|
|
tiny = Scale(large, image.Rect(0, 0, balance.LevelScreenshotTinySize.W, balance.LevelScreenshotTinySize.H), draw.ApproxBiLinear)
|
|
|
|
return large, medium, small, tiny, nil
|
2023-12-09 03:48:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|