From 407ef7f455181870ba99e7f600649ef005df01c3 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 17 Jun 2018 07:56:51 -0700 Subject: [PATCH] Milestone: Screenshot to PNG Test Feature --- .gitignore | 1 + Changes.md | 24 ++++++++++++++++ README.md | 11 +++++-- doodle.go | 50 +++++++++++++++++++++++--------- events/events.go | 28 ++++++++++++------ events/types.go | 17 +++++++---- fps.go | 2 +- screenshot.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 178 insertions(+), 30 deletions(-) create mode 100644 Changes.md create mode 100644 screenshot.go diff --git a/.gitignore b/.gitignore index 0480ebe..09b771b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ fonts/ +screenshot-*.png diff --git a/Changes.md b/Changes.md new file mode 100644 index 0000000..067d905 --- /dev/null +++ b/Changes.md @@ -0,0 +1,24 @@ +# Changes + +## v0.0.1-alpha - June 17 2018 + +* Add a debug overlay that shows FPS, coordinates, and useful info. +* Add FPS throttling to target 60 frames per second. +* Add `F12` for Screenshot key which saves the in-memory representation of + the pixels you've drawn to disk as a PNG image. +* Smoothly connect dots between periods where the mouse button was held down + but was moving too fast. + +## v0.0.0-alpha + +* Basic SDL canvas that draws pixels when you click and/or drag. +* The lines drawn aren't smooth, because the mouse cursor moves too fast. + +### Screenshot Feature + +Pressing `F12` takes a screenshot and saves it on disk as a PNG. + +It does **NOT** read the SDL canvas data for this, though. It uses an +internal representation of the pixels you've been drawing, and writes that +to the PNG. This is important because that same pixel data will be used for +the custom level format. diff --git a/README.md b/README.md index 91f931e..d979894 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,16 @@ As a rough idea of the milestones needed for this game to work: ## SDL Paint Program * [x] Create a basic SDL window that you can click on to color pixels. - * [ ] Connect the pixels while the mouse is down to cover gaps. -* [ ] Implement a "screenshot" button that translates the canvas to a PNG + * [x] Connect the pixels while the mouse is down to cover gaps. +* [x] Implement a "screenshot" button that translates the canvas to a PNG image on disk. + * `F12` key to take a screenshot of your drawing. + * It reproduces a PNG image using its in-memory knowledge of the pixels you + have drawn, *not* by reading the SDL canvas. This will be important for + making the custom level format later. + * The PNG I draw looks slightly different to what you see on the SDL canvas; + maybe difference between `Renderer.DrawLine()` and my own algorithm or + the anti-aliasing. * [ ] Create a custom map file format (protobufs maybe) and "screenshot" the canvas into this custom file format. * [ ] Make the program able to read this file format and reproduce the same diff --git a/doodle.go b/doodle.go index 0966273..74241d8 100644 --- a/doodle.go +++ b/doodle.go @@ -13,10 +13,10 @@ import ( const ( // Version number. - Version = "0.0.0-alpha" + Version = "0.0.1-alpha" // TargetFPS is the frame rate to cap the game to. - TargetFPS = uint32(1000 / 60) // 60 FPS + TargetFPS = 1000 / 60 // 60 FPS // Millisecond64 is a time.Millisecond casted to float64. Millisecond64 = float64(time.Millisecond) @@ -104,20 +104,27 @@ func (d *Doodle) Run() error { log.Info("Enter Main Loop") for d.running { + d.ticks++ + // Draw a frame and log how long it took. start := time.Now() err = d.Loop() - d.ticks++ - elapsed := time.Now().Sub(start) - - tmp := elapsed / time.Millisecond - delay := TargetFPS - uint32(tmp) - sdl.Delay(delay) - - d.TrackFPS(delay) if err != nil { return err } + + elapsed := time.Now().Sub(start) + + // Delay to maintain the target frames per second. + tmp := elapsed / time.Millisecond + var delay uint32 + if TargetFPS-int(tmp) > 0 { // make sure it won't roll under + delay = uint32(TargetFPS - int(tmp)) + } + sdl.Delay(delay) + + // Track how long this frame took to measure FPS over time. + d.TrackFPS(delay) } log.Warn("Main Loop Exited! Shutting down...") @@ -143,6 +150,7 @@ func (p Pixel) String() string { // Grid is a 2D grid of pixels in X,Y notation. type Grid map[Pixel]interface{} +// TODO: a linked list instead of a slice var pixelHistory []Pixel // Loop runs one loop of the game engine. @@ -154,6 +162,12 @@ func (d *Doodle) Loop() error { return err } + // Taking a screenshot? + if ev.ScreenshotKey.Pressed() { + log.Info("Taking a screenshot") + d.Screenshot() + } + // Clear the canvas and fill it with white. d.renderer.SetDrawColor(255, 255, 255, 255) d.renderer.Clear() @@ -161,16 +175,26 @@ func (d *Doodle) Loop() error { // Clicking? Log all the pixels while doing so. if ev.Button1.Now { pixel := Pixel{ - start: ev.Button1.Now && !ev.Button1.Last, + start: ev.Button1.Pressed(), x: ev.CursorX.Now, y: ev.CursorY.Now, - dx: ev.CursorX.Last, - dy: ev.CursorY.Last, + dx: ev.CursorX.Now, + dy: ev.CursorY.Now, } // Append unique new pixels. if len(pixelHistory) == 0 || pixelHistory[len(pixelHistory)-1] != pixel { + // If not a start pixel, make the delta coord the previous one. + if !pixel.start && len(pixelHistory) > 0 { + prev := pixelHistory[len(pixelHistory)-1] + pixel.dx = prev.x + pixel.dy = prev.y + } + pixelHistory = append(pixelHistory, pixel) + + // Save in the pixel canvas map. + d.canvas[pixel] = nil } } diff --git a/events/events.go b/events/events.go index dd138a2..5d5b5ab 100644 --- a/events/events.go +++ b/events/events.go @@ -10,21 +10,25 @@ import ( // State keeps track of event states. type State struct { // Mouse buttons. - Button1 *BoolFrameState - Button2 *BoolFrameState + Button1 *BoolTick + Button2 *BoolTick + + // Screenshot key. + ScreenshotKey *BoolTick // Cursor positions. - CursorX *Int32FrameState - CursorY *Int32FrameState + CursorX *Int32Tick + CursorY *Int32Tick } // New creates a new event state manager. func New() *State { return &State{ - Button1: &BoolFrameState{}, - Button2: &BoolFrameState{}, - CursorX: &Int32FrameState{}, - CursorY: &Int32FrameState{}, + Button1: &BoolTick{}, + Button2: &BoolTick{}, + ScreenshotKey: &BoolTick{}, + CursorX: &Int32Tick{}, + CursorY: &Int32Tick{}, } } @@ -96,6 +100,14 @@ func (s *State) Poll(ticks uint64) (*State, error) { log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", t.Timestamp, ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, ) + if t.Repeat == 1 { + continue + } + + switch t.Keysym.Scancode { + case sdl.SCANCODE_F12: + s.ScreenshotKey.Push(t.State == 1) + } } } diff --git a/events/types.go b/events/types.go index 0bc9021..4a413b6 100644 --- a/events/types.go +++ b/events/types.go @@ -1,25 +1,30 @@ package events -// BoolFrameState holds boolean state between this frame and the previous. -type BoolFrameState struct { +// BoolTick holds boolean state between this frame and the previous. +type BoolTick struct { Now bool Last bool } -// Int32FrameState manages int32 state between this frame and the previous. -type Int32FrameState struct { +// Int32Tick manages int32 state between this frame and the previous. +type Int32Tick struct { Now int32 Last int32 } // Push a bool state, copying the current Now value to Last. -func (bs *BoolFrameState) Push(v bool) { +func (bs *BoolTick) Push(v bool) { bs.Last = bs.Now bs.Now = v } +// Pressed returns true if the button was pressed THIS tick. +func (bs *BoolTick) Pressed() bool { + return bs.Now && !bs.Last +} + // Push an int32 state, copying the current Now value to Last. -func (is *Int32FrameState) Push(v int32) { +func (is *Int32Tick) Push(v int32) { is.Last = is.Now is.Now = v } diff --git a/fps.go b/fps.go index e88e79e..e7f7d85 100644 --- a/fps.go +++ b/fps.go @@ -27,7 +27,7 @@ func (d *Doodle) DrawDebugOverlay() { } text := fmt.Sprintf( - "FPS: %d (%dms) (X,Y)=(%d,%d) canvas=%d", + "FPS: %d (%dms) (%d,%d) size=%d F12=screenshot", fpsCurrent, fpsSkipped, d.events.CursorX.Now, diff --git a/screenshot.go b/screenshot.go new file mode 100644 index 0000000..67cfb96 --- /dev/null +++ b/screenshot.go @@ -0,0 +1,75 @@ +package doodle + +import ( + "fmt" + "image" + "image/png" + "math" + "os" + "time" +) + +// Screenshot saves the level canvas to disk as a PNG image. +func (d *Doodle) Screenshot() { + screenshot := image.NewRGBA(image.Rect(0, 0, int(d.width), int(d.height))) + + // White-out the image. + for x := 0; x < int(d.width); x++ { + for y := 0; y < int(d.height); y++ { + screenshot.Set(x, y, image.White) + } + } + + // Fill in the dots we drew. + for pixel := range d.canvas { + // A line or a dot? + if pixel.x == pixel.dx && pixel.y == pixel.dy { + screenshot.Set(int(pixel.x), int(pixel.y), image.Black) + } else { + // Draw a line. TODO: get this into its own function! + // https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm) + var ( + x1 = pixel.x + x2 = pixel.dx + y1 = pixel.y + y2 = pixel.dy + ) + var ( + dx = float64(x2 - x1) + dy = float64(y2 - y1) + ) + var step float64 + if math.Abs(dx) >= math.Abs(dy) { + step = math.Abs(dx) + } else { + step = math.Abs(dy) + } + + dx = dx / step + dy = dy / step + x := float64(x1) + y := float64(y1) + for i := 0; i <= int(step); i++ { + screenshot.Set(int(x), int(y), image.Black) + x += dx + y += dy + } + } + + } + + filename := fmt.Sprintf("screenshot-%s.png", + time.Now().Format("2006-01-02T15-04-05"), + ) + fh, err := os.Create(filename) + if err != nil { + log.Error(err.Error()) + return + } + defer fh.Close() + + if err := png.Encode(fh, screenshot); err != nil { + log.Error(err.Error()) + return + } +}