basic functionality

This commit is contained in:
Benedek László 2024-06-13 13:25:18 +02:00
parent 191e89faf6
commit 922b60f76a
12 changed files with 244 additions and 52 deletions

View File

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

View File

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

View File

@ -1,6 +0,0 @@
package api
type Coordinate struct {
X int `form:"x"`
Y int `form:"y"`
}

16
api/structs/structs.go Normal file
View 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
}

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,4 @@
#place {
border: 1px solid black;
image-rendering: pixelated;
}