Milestone: Screenshot to PNG Test Feature
This commit is contained in:
parent
f8fe40c5ef
commit
407ef7f455
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
fonts/
|
fonts/
|
||||||
|
screenshot-*.png
|
||||||
|
|
24
Changes.md
Normal file
24
Changes.md
Normal 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.
|
11
README.md
11
README.md
|
@ -7,9 +7,16 @@ As a rough idea of the milestones needed for this game to work:
|
||||||
## SDL Paint Program
|
## SDL Paint Program
|
||||||
|
|
||||||
* [x] Create a basic SDL window that you can click on to color pixels.
|
* [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.
|
* [x] Connect the pixels while the mouse is down to cover gaps.
|
||||||
* [ ] Implement a "screenshot" button that translates the canvas to a PNG
|
* [x] Implement a "screenshot" button that translates the canvas to a PNG
|
||||||
image on disk.
|
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
|
* [ ] Create a custom map file format (protobufs maybe) and "screenshot" the
|
||||||
canvas into this custom file format.
|
canvas into this custom file format.
|
||||||
* [ ] Make the program able to read this file format and reproduce the same
|
* [ ] Make the program able to read this file format and reproduce the same
|
||||||
|
|
50
doodle.go
50
doodle.go
|
@ -13,10 +13,10 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Version number.
|
// Version number.
|
||||||
Version = "0.0.0-alpha"
|
Version = "0.0.1-alpha"
|
||||||
|
|
||||||
// TargetFPS is the frame rate to cap the game to.
|
// 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 is a time.Millisecond casted to float64.
|
||||||
Millisecond64 = float64(time.Millisecond)
|
Millisecond64 = float64(time.Millisecond)
|
||||||
|
@ -104,20 +104,27 @@ func (d *Doodle) Run() error {
|
||||||
|
|
||||||
log.Info("Enter Main Loop")
|
log.Info("Enter Main Loop")
|
||||||
for d.running {
|
for d.running {
|
||||||
|
d.ticks++
|
||||||
|
|
||||||
// Draw a frame and log how long it took.
|
// Draw a frame and log how long it took.
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err = d.Loop()
|
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 {
|
if err != nil {
|
||||||
return err
|
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...")
|
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.
|
// Grid is a 2D grid of pixels in X,Y notation.
|
||||||
type Grid map[Pixel]interface{}
|
type Grid map[Pixel]interface{}
|
||||||
|
|
||||||
|
// TODO: a linked list instead of a slice
|
||||||
var pixelHistory []Pixel
|
var pixelHistory []Pixel
|
||||||
|
|
||||||
// Loop runs one loop of the game engine.
|
// Loop runs one loop of the game engine.
|
||||||
|
@ -154,6 +162,12 @@ func (d *Doodle) Loop() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Taking a screenshot?
|
||||||
|
if ev.ScreenshotKey.Pressed() {
|
||||||
|
log.Info("Taking a screenshot")
|
||||||
|
d.Screenshot()
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the canvas and fill it with white.
|
// Clear the canvas and fill it with white.
|
||||||
d.renderer.SetDrawColor(255, 255, 255, 255)
|
d.renderer.SetDrawColor(255, 255, 255, 255)
|
||||||
d.renderer.Clear()
|
d.renderer.Clear()
|
||||||
|
@ -161,16 +175,26 @@ func (d *Doodle) Loop() error {
|
||||||
// Clicking? Log all the pixels while doing so.
|
// Clicking? Log all the pixels while doing so.
|
||||||
if ev.Button1.Now {
|
if ev.Button1.Now {
|
||||||
pixel := Pixel{
|
pixel := Pixel{
|
||||||
start: ev.Button1.Now && !ev.Button1.Last,
|
start: ev.Button1.Pressed(),
|
||||||
x: ev.CursorX.Now,
|
x: ev.CursorX.Now,
|
||||||
y: ev.CursorY.Now,
|
y: ev.CursorY.Now,
|
||||||
dx: ev.CursorX.Last,
|
dx: ev.CursorX.Now,
|
||||||
dy: ev.CursorY.Last,
|
dy: ev.CursorY.Now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append unique new pixels.
|
// Append unique new pixels.
|
||||||
if len(pixelHistory) == 0 || pixelHistory[len(pixelHistory)-1] != pixel {
|
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)
|
pixelHistory = append(pixelHistory, pixel)
|
||||||
|
|
||||||
|
// Save in the pixel canvas map.
|
||||||
|
d.canvas[pixel] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,21 +10,25 @@ import (
|
||||||
// State keeps track of event states.
|
// State keeps track of event states.
|
||||||
type State struct {
|
type State struct {
|
||||||
// Mouse buttons.
|
// Mouse buttons.
|
||||||
Button1 *BoolFrameState
|
Button1 *BoolTick
|
||||||
Button2 *BoolFrameState
|
Button2 *BoolTick
|
||||||
|
|
||||||
|
// Screenshot key.
|
||||||
|
ScreenshotKey *BoolTick
|
||||||
|
|
||||||
// Cursor positions.
|
// Cursor positions.
|
||||||
CursorX *Int32FrameState
|
CursorX *Int32Tick
|
||||||
CursorY *Int32FrameState
|
CursorY *Int32Tick
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new event state manager.
|
// New creates a new event state manager.
|
||||||
func New() *State {
|
func New() *State {
|
||||||
return &State{
|
return &State{
|
||||||
Button1: &BoolFrameState{},
|
Button1: &BoolTick{},
|
||||||
Button2: &BoolFrameState{},
|
Button2: &BoolTick{},
|
||||||
CursorX: &Int32FrameState{},
|
ScreenshotKey: &BoolTick{},
|
||||||
CursorY: &Int32FrameState{},
|
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",
|
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,
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
package events
|
package events
|
||||||
|
|
||||||
// BoolFrameState holds boolean state between this frame and the previous.
|
// BoolTick holds boolean state between this frame and the previous.
|
||||||
type BoolFrameState struct {
|
type BoolTick struct {
|
||||||
Now bool
|
Now bool
|
||||||
Last bool
|
Last bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int32FrameState manages int32 state between this frame and the previous.
|
// Int32Tick manages int32 state between this frame and the previous.
|
||||||
type Int32FrameState struct {
|
type Int32Tick struct {
|
||||||
Now int32
|
Now int32
|
||||||
Last int32
|
Last int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push a bool state, copying the current Now value to Last.
|
// 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.Last = bs.Now
|
||||||
bs.Now = v
|
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.
|
// 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.Last = is.Now
|
||||||
is.Now = v
|
is.Now = v
|
||||||
}
|
}
|
||||||
|
|
2
fps.go
2
fps.go
|
@ -27,7 +27,7 @@ func (d *Doodle) DrawDebugOverlay() {
|
||||||
}
|
}
|
||||||
|
|
||||||
text := fmt.Sprintf(
|
text := fmt.Sprintf(
|
||||||
"FPS: %d (%dms) (X,Y)=(%d,%d) canvas=%d",
|
"FPS: %d (%dms) (%d,%d) size=%d F12=screenshot",
|
||||||
fpsCurrent,
|
fpsCurrent,
|
||||||
fpsSkipped,
|
fpsSkipped,
|
||||||
d.events.CursorX.Now,
|
d.events.CursorX.Now,
|
||||||
|
|
75
screenshot.go
Normal file
75
screenshot.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user