diff --git a/api/endpoints.go b/api/endpoints.go index 82dc58a..fd063e0 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -4,12 +4,23 @@ import ( "net/http" "time" + "git.tek.govt.hu/dowerx/place/api/read" + "git.tek.govt.hu/dowerx/place/api/write" "github.com/gin-gonic/gin" ) func Listen(address string) { router := gin.Default() + router.GET("/info", read.Info) + router.GET("/tile", read.Tile) + router.POST("/set", write.Pixel) + router.OPTIONS("/set", func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Headers", "*") + c.Header("Access-Control-Allow-Methods", "*") + }) + server := &http.Server{ Addr: address, Handler: router, diff --git a/api/read/read.go b/api/read/read.go index 223689d..8370afe 100644 --- a/api/read/read.go +++ b/api/read/read.go @@ -1,9 +1,37 @@ package read -import "github.com/gin-gonic/gin" +import ( + "git.tek.govt.hu/dowerx/place/api/structs" + "git.tek.govt.hu/dowerx/place/config" + "git.tek.govt.hu/dowerx/place/storage" + "github.com/gin-gonic/gin" +) + +func Info(c *gin.Context) { + conf := config.GetConfig() + c.JSON(200, gin.H{ + "tileSize": conf.TileSize, + "canvasSize": conf.CanvasSize, + "timeout": conf.Timeout, + }) +} func Tile(c *gin.Context) { + var coords structs.Coordinates + if c.ShouldBind(&coords) != nil { + c.AbortWithStatus(400) + return + } + buffer, err := storage.GetTile(coords.X, coords.Y) + if err != nil { + c.AbortWithError(500, err) + return + } + + c.Status(200) + c.Header("Content-Type", "image/png") + c.Writer.Write(buffer.Bytes()) } func Continuous(c *gin.Context) { diff --git a/api/structs.go b/api/structs.go deleted file mode 100644 index 737be40..0000000 --- a/api/structs.go +++ /dev/null @@ -1,6 +0,0 @@ -package api - -type Coordinate struct { - X int `form:"x"` - Y int `form:"y"` -} diff --git a/api/structs/structs.go b/api/structs/structs.go new file mode 100644 index 0000000..8937ca8 --- /dev/null +++ b/api/structs/structs.go @@ -0,0 +1,16 @@ +package structs + +type Coordinates struct { + X int `form:"x" json:"x"` + Y int `form:"y" json:"y"` +} + +type Color struct { + R uint32 `form:"r" json:"r"` + G uint32 `form:"g" json:"g"` + B uint32 `form:"b" json:"b"` +} + +func (c *Color) RGBA() (r, g, b, a uint32) { + return c.R, c.G, c.B, 65535 +} diff --git a/api/write/write.go b/api/write/write.go index f28e5bd..4b16433 100644 --- a/api/write/write.go +++ b/api/write/write.go @@ -1,11 +1,33 @@ package write -import "github.com/gin-gonic/gin" +import ( + "errors" + + "git.tek.govt.hu/dowerx/place/api/structs" + "git.tek.govt.hu/dowerx/place/storage" + "github.com/gin-gonic/gin" +) func Pixel(c *gin.Context) { + // TODO: apply timeout using cookies + var info struct { + structs.Color + structs.Coordinates + } + if c.ShouldBind(&info) != nil { + c.AbortWithStatus(400) + return + } + + if err := storage.SetPixel(info.X, info.Y, &info); err != nil { + c.AbortWithError(500, err) + return + } + + c.Status(200) } func Bitmap(c *gin.Context) { - + panic(errors.New("unimplomented")) } diff --git a/config/config.go b/config/config.go index 51c6c77..272e863 100644 --- a/config/config.go +++ b/config/config.go @@ -24,7 +24,7 @@ func GetConfig() Config { flag.StringVar(&conf.Address, "address", ":8080", "API base") flag.StringVar(&conf.StoragePath, "storage", "/data", "image storage path") flag.IntVar(&conf.TileSize, "tile_size", 128, "width of a tile") - flag.IntVar(&conf.CanvasSize, "canvas_size", 16, "width of the canvas (in tiles)") + flag.IntVar(&conf.CanvasSize, "canvas_size", 4, "width of the canvas (in tiles)") flag.IntVar(&conf.SaveFrequency, "save", 60, "seconds between saves") flag.IntVar(&conf.Timeout, "timeout", 60, "timeout after placing a pixel") flag.StringVar(&conf.Redis.Addr, "redis_address", "redis:6379", "address of the redis server") diff --git a/main.go b/main.go index 0eb43a4..b1562b7 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "git.tek.govt.hu/dowerx/place/api" "git.tek.govt.hu/dowerx/place/config" "git.tek.govt.hu/dowerx/place/storage" ) @@ -8,4 +9,6 @@ import ( func main() { conf := config.GetConfig() storage.Load(conf.StoragePath, conf.CanvasSize, conf.TileSize) + storage.StartSaves(conf.SaveFrequency) + api.Listen(conf.Address) } diff --git a/storage/storage.go b/storage/storage.go index b0ce68f..08ae021 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1,22 +1,18 @@ package storage import ( + "bytes" "errors" "fmt" "image" "image/color" "image/draw" - "image/png" "log" "os" + "time" ) -type Tile struct { - Dirty bool - Image *image.RGBA -} - -var tiles [][]Tile +var tiles [][]tile type WrongSizeError struct { ExpectedSize int @@ -34,25 +30,19 @@ func Load(path string, canvasSize int, tileSize int) { panic(err) } - tiles = make([][]Tile, canvasSize) + tiles = make([][]tile, canvasSize) for i := range tiles { - tiles[i] = make([]Tile, canvasSize) + tiles[i] = make([]tile, canvasSize) } for y := range tiles { for x := range tiles[y] { - tiles[y][x].Image = image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) + tiles[y][x].image = image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) filename := fmt.Sprintf("%d-%d.png", x, y) file, err := os.Open(filename) if err != nil { - file, err = os.Create(filename) - if err != nil { - panic(err) - } - - draw.Draw(tiles[y][x].Image, tiles[y][x].Image.Bounds(), &image.Uniform{C: image.White}, image.Point{}, draw.Src) - - if err = png.Encode(file, tiles[y][x].Image); err != nil { + tiles[y][x].fill() + if err = tiles[y][x].save(filename); err != nil { panic(err) } @@ -63,15 +53,15 @@ func Load(path string, canvasSize int, tileSize int) { if err != nil { panic(err) } - draw.Draw(tiles[y][x].Image, img.Bounds(), img, img.Bounds().Min, draw.Src) + draw.Draw(tiles[y][x].image, img.Bounds(), img, img.Bounds().Min, draw.Src) } defer file.Close() - if tiles[y][x].Image.Bounds().Size().X != tileSize && tiles[y][x].Image.Bounds().Size().Y != tileSize { - panic(WrongSizeError{ExpectedSize: tileSize, ActualSize: tiles[y][x].Image.Bounds().Size(), X: x, Y: y}) + if tiles[y][x].image.Bounds().Size().X != tileSize && tiles[y][x].image.Bounds().Size().Y != tileSize { + panic(WrongSizeError{ExpectedSize: tileSize, ActualSize: tiles[y][x].image.Bounds().Size(), X: x, Y: y}) } - tiles[y][x].Dirty = false + tiles[y][x].dirty = false } } } @@ -79,42 +69,48 @@ func Load(path string, canvasSize int, tileSize int) { func Save() { for y := range tiles { for x := range tiles[y] { - if tiles[y][x].Dirty { + if tiles[y][x].dirty { filename := fmt.Sprintf("%d-%d.png", x, y) - file, err := os.OpenFile(filename, os.O_WRONLY, 0644) - if err != nil { + + if err := tiles[y][x].save(filename); err != nil { panic(err) } - defer file.Close() - - if err = png.Encode(file, tiles[y][x].Image); err != nil { - panic(err) - } - - tiles[y][x].Dirty = false } } } } -func GetTile(x int, y int) (*Tile, error) { +func StartSaves(saveFrequency int) { + go func() { + for range time.Tick(time.Second * time.Duration(saveFrequency)) { + Save() + } + }() +} + +func GetTile(x int, y int) (*bytes.Buffer, error) { if x >= len(tiles) || y >= len(tiles) { return nil, errors.New("tile coordinates out of range") } - return &tiles[y][x], nil + return tiles[y][x].toBytesPNG() } -func SetPixel(tx int, ty int, x int, y int, c color.Color) error { - if ty >= len(tiles) || tx >= len(tiles[y]) { - return errors.New("tile coordinates out of range") - } - if x >= tiles[y][x].Image.Bounds().Size().X || y >= tiles[y][x].Image.Bounds().Size().Y { - return errors.New("pixel coordinates out of range") +func SetPixel(x int, y int, c color.Color) error { + size := tiles[0][0].size() + + var tx int = x / size.X + var ty int = y / size.Y + + x = x % size.X + y = y % size.Y + + if ty >= len(tiles) || tx >= len(tiles[ty]) { + return errors.New("coordinates out of range") } - tiles[y][x].Image.Set(x, y, c) - tiles[y][x].Dirty = true + tiles[ty][tx].image.Set(x, y, c) + tiles[ty][tx].dirty = true return nil } diff --git a/storage/tile.go b/storage/tile.go new file mode 100644 index 0000000..57dbb08 --- /dev/null +++ b/storage/tile.go @@ -0,0 +1,40 @@ +package storage + +import ( + "bytes" + "image" + "image/draw" + "image/png" + "os" +) + +type tile struct { + dirty bool + image *image.RGBA +} + +func (t *tile) save(path string) error { + file, err := os.OpenFile(path, os.O_WRONLY, 0644) + if err != nil { + return err + } + defer file.Close() + + err = png.Encode(file, t.image) + t.dirty = false + return err +} + +func (t *tile) fill() { + draw.Draw(t.image, t.image.Bounds(), &image.Uniform{C: image.White}, image.Point{}, draw.Src) +} + +func (t *tile) toBytesPNG() (*bytes.Buffer, error) { + buffer := &bytes.Buffer{} + err := png.Encode(buffer, t.image) + return buffer, err +} + +func (t *tile) size() image.Point { + return t.image.Bounds().Size() +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e131eb8 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + Place + + + + + + + \ No newline at end of file diff --git a/web/place.js b/web/place.js new file mode 100644 index 0000000..196dbd6 --- /dev/null +++ b/web/place.js @@ -0,0 +1,65 @@ +const apiBase = "http://localhost:8080"; + +var tiles = []; +var color = { + r: 0, + g: 0, + b: 0 +}; + +async function getInfo() { + let response = await fetch(`${apiBase}/info`); + return await response.json(); +} + +async function setPixel(canvas, ctx, x, y, c) { + let bounds = canvas.getBoundingClientRect(); + x = x - bounds.left; + y = y - bounds.top; + + try { + await fetch(`${apiBase}/set`, { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + x: x, + y: y, + r: c.r, + g: c.g, + b: c.b + }) + }); + + ctx.fillStyle = `rgb(${c.r}, ${c.g}, ${c.b})`; + ctx.fillRect(x, y, 1, 1); + } catch { + alert(`failed to set pixel at ${x}-${y} to ${c}`); + } +} + +function init(info, ctx) { + for (let y = 0; y < info.canvasSize; y++) { + tiles.push([]); + for (let x = 0; x < info.canvasSize; x++) { + let tile = new Image(); + tile.onload = () => ctx.drawImage(tile, x * info.tileSize, y * info.tileSize); + tile.src = `${apiBase}/tile?x=${x}&y=${y}`; + tiles[y].push(tile); + } + } +} + +async function main() { + let info = await getInfo(); + console.log(info); + + let canvas = document.getElementById("place"); + let ctx = canvas.getContext("2d"); + init(info, ctx); + + canvas.onclick = e => setPixel(canvas, ctx, e.clientX, e.clientY, color); +} + +main(); \ No newline at end of file diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..d7e53f8 --- /dev/null +++ b/web/style.css @@ -0,0 +1,4 @@ +#place { + border: 1px solid black; + image-rendering: pixelated; +} \ No newline at end of file