Milestone: Screenshot to PNG Test Feature

chunks
Noah 2018-06-17 07:56:51 -07:00
parent f8fe40c5ef
commit 407ef7f455
8 changed files with 178 additions and 30 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
fonts/
screenshot-*.png

24
Changes.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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
}

2
fps.go
View File

@ -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,

75
screenshot.go Normal file
View File

@ -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
}
}