package giant_screenshot import ( "errors" "image" "image/draw" "image/png" "os" "path/filepath" "time" "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/shmem" "git.kirsle.net/SketchyMaze/doodle/pkg/userdir" "git.kirsle.net/SketchyMaze/doodle/pkg/wallpaper" "git.kirsle.net/go/render" ) /* Giant Screenshot functionality for the Level Editor. */ var locked bool // GiantScreenshot returns a rendered RGBA image of the entire level. // // 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)...") // How big will our image be? var ( size = lvl.Chunker.WorldSizePositive() chunkSize = int(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.Origin) // 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 := dpp.Driver.LoadFromEmbeddable(actor.Filename, lvl, false) if err != nil { log.Error("GiantScreenshot: Load doodad: %s", err) continue } // Offset the doodad position if the image is displaying // negative coordinates. drawAt := render.NewPoint(actor.Point.X, actor.Point.Y) if worldSize.X < 0 { var offset = render.AbsInt(worldSize.X) * chunkSize drawAt.X += offset } if worldSize.Y < 0 { var offset = render.AbsInt(worldSize.Y) * chunkSize drawAt.Y += offset } // 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(drawAt.X, drawAt.Y)) } } return img, nil } // 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, err := GiantScreenshot(level) 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 } // 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. // // 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) if err != nil { log.Error("GiantScreenshot: wallpaper load: %s", err) } var size = wp.QuarterRect() var resultImage = target // 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) } } // Tile the left edge for bounded lvls. if lvl.PageType > level.Unbounded { // 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) } } // The top edge. 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) } } // The top left corner. if absOffset.X < size.W && absOffset.Y < size.H { resultImage = blotImage(resultImage, wp.Corner(), image.Pt( -offset.X, -offset.Y, )) } } 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 }