diff --git a/io.go b/io.go deleted file mode 100644 index 60fe6e1..0000000 --- a/io.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "image" - _ "image/gif" - _ "image/jpeg" - "image/png" - "log" - "os" -) - -func readImage(path string) (img image.Image) { - reader, err := os.Open(path) - if err != nil { - log.Fatal(err) - } - img, _, err2 := image.Decode(reader) - if err2 != nil { - log.Fatal(err2) - } - return img -} - -func writeImage(path string, img image.Image) { - f, err := os.Create(path) - if err != nil { - log.Fatal(err) - } - if err := png.Encode(f, img); err != nil { - f.Close() - log.Fatal(err) - } -} - -func imgToNRGBA(img image.Image) *image.NRGBA { - b := img.Bounds() - r := image.NewNRGBA(b) - for x := b.Min.X; x < b.Max.X; x++ { - for y := b.Min.Y; y < b.Max.Y; y++ { - r.Set(x, y, img.At(x, y)) - } - } - return r -} diff --git a/main.go b/main.go index a7ffc34..26649e1 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/SpeckiJ/Hochwasser/pixelflut" + "github.com/SpeckiJ/Hochwasser/render" "github.com/SpeckiJ/Hochwasser/rpc" ) @@ -50,7 +51,12 @@ func main() { if *imgPath != "" { offset := image.Pt(*x, *y) - img := imgToNRGBA(readImage(*imgPath)) + imgTmp, err := render.ReadImage(*imgPath) + if err != nil { + log.Println(err) + return + } + img := render.ImgToNRGBA(imgTmp) // check connectivity by opening one test connection conn, err := net.Dial("tcp", *address) @@ -71,7 +77,7 @@ func main() { if *fetchImgPath != "" { fetchedImg := pixelflut.FetchImage(img.Bounds().Add(offset), *address, 1, stopChan) *connections -= 1 - defer writeImage(*fetchImgPath, fetchedImg) + defer render.WriteImage(*fetchImgPath, fetchedImg) } // local 🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊 diff --git a/render/image.go b/render/image.go new file mode 100644 index 0000000..b7aad79 --- /dev/null +++ b/render/image.go @@ -0,0 +1,54 @@ +package render + +import ( + "image" + _ "image/gif" // register gif, jpeg, png format handlers + _ "image/jpeg" + "image/png" + "os" + + "golang.org/x/image/draw" +) + +func ReadImage(path string) (image.Image, error) { + reader, err := os.Open(path) + if err != nil { + return nil, err + } + img, _, err := image.Decode(reader) + if err != nil { + return nil, err + } + return img, nil +} + +func WriteImage(path string, img image.Image) error { + f, err := os.Create(path) + if err != nil { + return err + } + if err := png.Encode(f, img); err != nil { + f.Close() + return err + } + return nil +} + +func ImgToNRGBA(img image.Image) *image.NRGBA { + b := img.Bounds() + r := image.NewNRGBA(b) + for x := b.Min.X; x < b.Max.X; x++ { + for y := b.Min.Y; y < b.Max.Y; y++ { + r.Set(x, y, img.At(x, y)) + } + } + return r +} + +func ScaleImage(img image.Image, factor int) (scaled *image.NRGBA) { + b := img.Bounds() + scaledBounds := image.Rect(0, 0, b.Max.X*factor, b.Max.Y*factor) + scaledImg := image.NewNRGBA(scaledBounds) + draw.NearestNeighbor.Scale(scaledImg, scaledBounds, img, b, draw.Src, nil) + return scaledImg +} diff --git a/render/text.go b/render/text.go new file mode 100644 index 0000000..e66ea00 --- /dev/null +++ b/render/text.go @@ -0,0 +1,42 @@ +package render + +import ( + "image" + "image/color" + + "golang.org/x/image/draw" + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" + "golang.org/x/image/math/fixed" +) + +func pt(p fixed.Point26_6) image.Point { + return image.Point{ + X: int(p.X+32) >> 6, + Y: int(p.Y+32) >> 6, + } +} + +func RenderText(text string, scale int, col color.Color) *image.NRGBA { + // @incomplete: draw with texture via Drawer.Src + face := basicfont.Face7x13 + stringBounds, _ := font.BoundString(face, text) + + b := image.Rectangle{pt(stringBounds.Min), pt(stringBounds.Max)} + img := image.NewNRGBA(b) + + draw.Draw(img, b, image.Black, image.Point{}, draw.Src) // fill with black bg + + d := font.Drawer{ + Dst: img, + Src: image.NewUniform(col), + Face: face, + } + d.DrawString(text) + + // normalize bounds to start at 0,0 + img.Rect = img.Bounds().Sub(img.Bounds().Min) + + // scale up, as this font is quite small + return ScaleImage(img, scale) +} diff --git a/rpc/dottir.go b/rpc/dottir.go index 392fd10..1d0c157 100644 --- a/rpc/dottir.go +++ b/rpc/dottir.go @@ -46,11 +46,9 @@ type FlutStatus struct { } func (h *Hevring) Flut(task FlutTask, reply *FlutAck) error { - if (h.task != FlutTask{}) { - // @incomplete: stop old task if new task is received - fmt.Println("[hevring] already have a task") - reply.Ok = false - return nil + // stop old task if new task is received + if h.taskQuit != nil { + close(h.taskQuit) } fmt.Printf("[hevring] Rán gave us /w o r k/! %v\n", task) @@ -71,7 +69,7 @@ func (h *Hevring) Status(metrics bool, reply *FlutStatus) error { } func (h *Hevring) Stop(x int, reply *FlutAck) error { - if (h.task != FlutTask{}) { + if h.taskQuit != nil { fmt.Println("[hevring] stopping task") h.task = FlutTask{} close(h.taskQuit) @@ -82,7 +80,9 @@ func (h *Hevring) Stop(x int, reply *FlutAck) error { } func (h *Hevring) Die(x int, reply *FlutAck) error { - go func() { // @cleanup: hacky + // @robustness: waiting for reply to be sent via timeout + // @incomplete: should try to reconnect for a bit first + go func() { time.Sleep(100 * time.Millisecond) fmt.Println("[hevring] Rán disconnected, stopping") os.Exit(0) diff --git a/rpc/ran.go b/rpc/ran.go index 3e046f3..979ed3a 100644 --- a/rpc/ran.go +++ b/rpc/ran.go @@ -1,20 +1,19 @@ package rpc import ( - "bufio" "fmt" "image" "log" "net" "net/rpc" - "os" - "strings" "sync" "time" "github.com/SpeckiJ/Hochwasser/pixelflut" ) +// Rán represents the RPC hub, used to coordinate `Hevring` clients. +// Implements `Fluter` type Rán struct { clients []*rpc.Client task FlutTask @@ -93,70 +92,23 @@ func SummonRán(address string, stopChan chan bool, wg *sync.WaitGroup) *Rán { } }() - // REPL to change tasks without loosing clients - go func() { - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - input := strings.Split(scanner.Text(), " ") - cmd := strings.ToLower(input[0]) - args := input[1:] - if cmd == "stop" { - for _, c := range r.clients { - ack := FlutAck{} - c.Call("Hevring.Stop", 0, &ack) // @speed: async - } - - } else if cmd == "start" { - if (r.task != FlutTask{}) { - for _, c := range r.clients { - ack := FlutAck{} - // @speed: should send tasks async - err := c.Call("Hevring.Flut", r.task, &ack) - if err != nil || !ack.Ok { - log.Printf("[rán] client didn't accept task") - } - } - } - - } else if cmd == "img" && len(args) > 0 { - // // @incomplete - // path := args[0] - // img := readImage(path) - // offset := image.Pt(0, 0) - // if len(args) == 3 { - // x := strconv.Atoi(args[1]) - // y := strconv.Atoi(args[2]) - // offset = image.Pt(x, y) - // } - // task := FlutTask{} - - } else if cmd == "metrics" { - r.metrics.Enabled = !r.metrics.Enabled - - } - } - }() - - // kill clients on exit - go func() { - <-stopChan - for _, c := range r.clients { - ack := FlutAck{} - c.Call("Hevring.Die", 0, &ack) // @speed: async - } - wg.Done() - }() + go RunREPL(r) + go r.killClients(stopChan, wg) return r } -// SetTask assigns a FlutTask to Rán, distributing it to all clients -func (r *Rán) SetTask(img *image.NRGBA, offset image.Point, address string, maxConns int) { - // @incomplete: smart task creation: - // fetch server state & sample foreign activity in image regions. assign - // subregions to clients (per connection), considering their bandwidth. +func (r *Rán) getTask() FlutTask { return r.task } - r.task = FlutTask{address, maxConns, img, offset, true} +func (r *Rán) toggleMetrics() { + r.metrics.Enabled = !r.metrics.Enabled +} + +func (r *Rán) applyTask(t FlutTask) { + if (t == FlutTask{}) { // @robustness: FlutTask should provide .IsValid() + return + } + r.task = t for _, c := range r.clients { ack := FlutAck{} // @speed: should send tasks async @@ -166,3 +118,29 @@ func (r *Rán) SetTask(img *image.NRGBA, offset image.Point, address string, max } } } + +func (r *Rán) stopTask() { + // @robustness: errorchecking + for _, c := range r.clients { + ack := FlutAck{} + c.Call("Hevring.Stop", 0, &ack) // @speed: async + } +} + +func (r *Rán) killClients(stopChan <-chan bool, wg *sync.WaitGroup) { + <-stopChan + for _, c := range r.clients { + ack := FlutAck{} + c.Call("Hevring.Die", 0, &ack) // @speed: async + } + wg.Done() +} + +// SetTask assigns a FlutTask to Rán, distributing it to all clients +func (r *Rán) SetTask(img *image.NRGBA, offset image.Point, address string, maxConns int) { + // @incomplete: smart task creation: + // fetch server state & sample foreign activity in image regions. assign + // subregions to clients (per connection), considering their bandwidth. + + r.applyTask(FlutTask{address, maxConns, img, offset, true}) +} diff --git a/rpc/repl.go b/rpc/repl.go new file mode 100644 index 0000000..0405f7c --- /dev/null +++ b/rpc/repl.go @@ -0,0 +1,116 @@ +package rpc + +import ( + "bufio" + "encoding/hex" + "fmt" + "image" + "image/color" + "os" + "strconv" + "strings" + + "github.com/SpeckiJ/Hochwasser/render" +) + +// Fluter implements flut operations that can be triggered via a REPL +type Fluter interface { + getTask() FlutTask + applyTask(FlutTask) + stopTask() + toggleMetrics() +} + +const commandMode = "CMD" +const textMode = "TXT" + +// RunREPL starts reading os.Stdin for commands to apply to the given Fluter +func RunREPL(f Fluter) { + mode := commandMode + textSize := 4 + textCol := color.NRGBA{0xff, 0xff, 0xff, 0xff} + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + inputStr := scanner.Text() + + switch mode { + case textMode: + if inputStr == commandMode { + fmt.Println("[rán] command mode") + mode = commandMode + continue + } + t := f.getTask() + t.Img = render.RenderText(inputStr, textSize, textCol) + f.applyTask(t) + + case commandMode: + input := strings.Split(inputStr, " ") + cmd := strings.ToLower(input[0]) + args := input[1:] + switch cmd { + case "stop": + f.stopTask() + + case "start": + f.applyTask(f.getTask()) + + case "offset": + if len(args) == 2 { + x, err := strconv.Atoi(args[0]) + y, err2 := strconv.Atoi(args[1]) + if err == nil && err2 == nil { + t := f.getTask() + t.Offset = image.Pt(x, y) + f.applyTask(t) + } + } + + case "conns": + if len(args) == 1 { + if conns, err := strconv.Atoi(args[0]); err == nil { + t := f.getTask() + t.MaxConns = conns + f.applyTask(t) + } + } + + case "shuffle": + t := f.getTask() + t.Shuffle = !t.Shuffle + f.applyTask(t) + + case "txt": + fmt.Printf("[rán] text mode, return via %v\n", commandMode) + mode = textMode + if len(args) > 0 { + if size, err := strconv.Atoi(args[0]); err == nil { + textSize = size + } + } + if len(args) > 1 { + if col, err := hex.DecodeString(args[1]); err == nil { + textCol = color.NRGBA{col[0], col[1], col[2], 0xff} + } + } + + case "img": + if len(args) > 0 { + path := strings.Join(args, " ") + t := f.getTask() + if img, err := render.ReadImage(path); err != nil { + fmt.Println(err) + } else { + t.Img = render.ImgToNRGBA(img) + f.applyTask(t) + } + } + + case "metrics": + f.toggleMetrics() + + } + } + } +}