basic functionality
This commit is contained in:
parent
191e89faf6
commit
922b60f76a
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
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"))
|
||||
}
|
||||
|
@ -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")
|
||||
|
3
main.go
3
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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
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