doodle/pkg/level/giant_screenshot/regular_screenshot.go
Noah Petherbridge a06787411d Resolve circular import errors for Doodle++ plugin
* pkg/plus/dpp is the main plugin bridge, and defines nothing but an interface
  that defines the Doodle++ surface area (referring to internal game types such
  as doodad.Doodad or level.Level), but not their implementations.
  * dpp.Driver (an interface) is the main API that other parts of the game will
    call, for example "dpp.Driver.IsLevelSigned()"
  * plus_dpp.go and plus_foss.go provide the dpp.Driver implementation for their
    build; with plus_dpp.go generally forwarding function calls directly to the
    proprietary dpp package and plus_foss.go generally returning false/errors.
  * The bootstrap package simply assigns the above stub function to dpp.Driver
* pkg/plus/bootstrap is a package directly imported by main (in the doodle and
  doodad programs) and it works around circular dependency issues: this package
  simply assigns dpp.Driver to the DPP or FOSS version.

Miscellaneous fixes:

* File->Open in the editor and PlayScene will use the new Open Level window
  instead of loading the legacy GotoLoadMenu scene.
* Deprecated legacy scenes: d.GotoLoadMenu() and d.GotoPlayMenu().
* The doodle-admin program depends on the private dpp package, so can not be
  compiled in FOSS mode.
2024-04-18 22:12:56 -07:00

205 lines
6.0 KiB
Go

package giant_screenshot
import (
"bytes"
"errors"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"runtime"
"time"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/plus/dpp"
"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) {
// Not for WASM for now.
if runtime.GOOS == "js" {
return nil, errors.New("screenshots not yet supported for WASM")
}
// 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 := dpp.Driver.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, tiny, 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,
balance.LevelScreenshotTinyFilename: tiny,
} {
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, tiny 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)
tiny = Scale(large, image.Rect(0, 0, balance.LevelScreenshotTinySize.W, balance.LevelScreenshotTinySize.H), draw.ApproxBiLinear)
return large, medium, small, tiny, 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
}