basic functionality
This commit is contained in:
parent
191e89faf6
commit
922b60f76a
@ -4,12 +4,23 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.tek.govt.hu/dowerx/place/api/read"
|
||||||
|
"git.tek.govt.hu/dowerx/place/api/write"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Listen(address string) {
|
func Listen(address string) {
|
||||||
router := gin.Default()
|
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{
|
server := &http.Server{
|
||||||
Addr: address,
|
Addr: address,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
|
@ -1,9 +1,37 @@
|
|||||||
package read
|
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) {
|
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) {
|
func Continuous(c *gin.Context) {
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
type Coordinate struct {
|
|
||||||
X int `form:"x"`
|
|
||||||
Y int `form:"y"`
|
|
||||||
}
|
|
16
api/structs/structs.go
Normal file
16
api/structs/structs.go
Normal file
@ -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
|
||||||
|
}
|
@ -1,11 +1,33 @@
|
|||||||
package write
|
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) {
|
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) {
|
func Bitmap(c *gin.Context) {
|
||||||
|
panic(errors.New("unimplomented"))
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ func GetConfig() Config {
|
|||||||
flag.StringVar(&conf.Address, "address", ":8080", "API base")
|
flag.StringVar(&conf.Address, "address", ":8080", "API base")
|
||||||
flag.StringVar(&conf.StoragePath, "storage", "/data", "image storage path")
|
flag.StringVar(&conf.StoragePath, "storage", "/data", "image storage path")
|
||||||
flag.IntVar(&conf.TileSize, "tile_size", 128, "width of a tile")
|
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.SaveFrequency, "save", 60, "seconds between saves")
|
||||||
flag.IntVar(&conf.Timeout, "timeout", 60, "timeout after placing a pixel")
|
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")
|
flag.StringVar(&conf.Redis.Addr, "redis_address", "redis:6379", "address of the redis server")
|
||||||
|
3
main.go
3
main.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.tek.govt.hu/dowerx/place/api"
|
||||||
"git.tek.govt.hu/dowerx/place/config"
|
"git.tek.govt.hu/dowerx/place/config"
|
||||||
"git.tek.govt.hu/dowerx/place/storage"
|
"git.tek.govt.hu/dowerx/place/storage"
|
||||||
)
|
)
|
||||||
@ -8,4 +9,6 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
conf := config.GetConfig()
|
conf := config.GetConfig()
|
||||||
storage.Load(conf.StoragePath, conf.CanvasSize, conf.TileSize)
|
storage.Load(conf.StoragePath, conf.CanvasSize, conf.TileSize)
|
||||||
|
storage.StartSaves(conf.SaveFrequency)
|
||||||
|
api.Listen(conf.Address)
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"image/png"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tile struct {
|
var tiles [][]tile
|
||||||
Dirty bool
|
|
||||||
Image *image.RGBA
|
|
||||||
}
|
|
||||||
|
|
||||||
var tiles [][]Tile
|
|
||||||
|
|
||||||
type WrongSizeError struct {
|
type WrongSizeError struct {
|
||||||
ExpectedSize int
|
ExpectedSize int
|
||||||
@ -34,25 +30,19 @@ func Load(path string, canvasSize int, tileSize int) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles = make([][]Tile, canvasSize)
|
tiles = make([][]tile, canvasSize)
|
||||||
for i := range tiles {
|
for i := range tiles {
|
||||||
tiles[i] = make([]Tile, canvasSize)
|
tiles[i] = make([]tile, canvasSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
for y := range tiles {
|
for y := range tiles {
|
||||||
for x := range tiles[y] {
|
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)
|
filename := fmt.Sprintf("%d-%d.png", x, y)
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
file, err = os.Create(filename)
|
tiles[y][x].fill()
|
||||||
if err != nil {
|
if err = tiles[y][x].save(filename); 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 {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,15 +53,15 @@ func Load(path string, canvasSize int, tileSize int) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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()
|
defer file.Close()
|
||||||
|
|
||||||
if tiles[y][x].Image.Bounds().Size().X != tileSize && tiles[y][x].Image.Bounds().Size().Y != tileSize {
|
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})
|
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() {
|
func Save() {
|
||||||
for y := range tiles {
|
for y := range tiles {
|
||||||
for x := range tiles[y] {
|
for x := range tiles[y] {
|
||||||
if tiles[y][x].Dirty {
|
if tiles[y][x].dirty {
|
||||||
filename := fmt.Sprintf("%d-%d.png", x, y)
|
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)
|
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) {
|
if x >= len(tiles) || y >= len(tiles) {
|
||||||
return nil, errors.New("tile coordinates out of range")
|
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 {
|
func SetPixel(x int, y int, c color.Color) error {
|
||||||
if ty >= len(tiles) || tx >= len(tiles[y]) {
|
size := tiles[0][0].size()
|
||||||
return errors.New("tile coordinates out of range")
|
|
||||||
}
|
var tx int = x / size.X
|
||||||
if x >= tiles[y][x].Image.Bounds().Size().X || y >= tiles[y][x].Image.Bounds().Size().Y {
|
var ty int = y / size.Y
|
||||||
return errors.New("pixel coordinates out of range")
|
|
||||||
|
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[ty][tx].image.Set(x, y, c)
|
||||||
tiles[y][x].Dirty = true
|
tiles[ty][tx].dirty = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
40
storage/tile.go
Normal file
40
storage/tile.go
Normal file
@ -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()
|
||||||
|
}
|
13
web/index.html
Normal file
13
web/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Place</title>
|
||||||
|
<script defer src="place.js"></script>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="place" width="512" height="512"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
65
web/place.js
Normal file
65
web/place.js
Normal file
@ -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();
|
4
web/style.css
Normal file
4
web/style.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#place {
|
||||||
|
border: 1px solid black;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user