add text rendering via repl
This commit is contained in:
parent
8a22c7bf29
commit
470115a126
44
io.go
44
io.go
|
@ -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
|
|
||||||
}
|
|
10
main.go
10
main.go
|
@ -11,6 +11,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/SpeckiJ/Hochwasser/pixelflut"
|
"github.com/SpeckiJ/Hochwasser/pixelflut"
|
||||||
|
"github.com/SpeckiJ/Hochwasser/render"
|
||||||
"github.com/SpeckiJ/Hochwasser/rpc"
|
"github.com/SpeckiJ/Hochwasser/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,7 +51,12 @@ func main() {
|
||||||
|
|
||||||
if *imgPath != "" {
|
if *imgPath != "" {
|
||||||
offset := image.Pt(*x, *y)
|
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
|
// check connectivity by opening one test connection
|
||||||
conn, err := net.Dial("tcp", *address)
|
conn, err := net.Dial("tcp", *address)
|
||||||
|
@ -71,7 +77,7 @@ func main() {
|
||||||
if *fetchImgPath != "" {
|
if *fetchImgPath != "" {
|
||||||
fetchedImg := pixelflut.FetchImage(img.Bounds().Add(offset), *address, 1, stopChan)
|
fetchedImg := pixelflut.FetchImage(img.Bounds().Add(offset), *address, 1, stopChan)
|
||||||
*connections -= 1
|
*connections -= 1
|
||||||
defer writeImage(*fetchImgPath, fetchedImg)
|
defer render.WriteImage(*fetchImgPath, fetchedImg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// local 🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊
|
// local 🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -46,11 +46,9 @@ type FlutStatus struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hevring) Flut(task FlutTask, reply *FlutAck) error {
|
func (h *Hevring) Flut(task FlutTask, reply *FlutAck) error {
|
||||||
if (h.task != FlutTask{}) {
|
// stop old task if new task is received
|
||||||
// @incomplete: stop old task if new task is received
|
if h.taskQuit != nil {
|
||||||
fmt.Println("[hevring] already have a task")
|
close(h.taskQuit)
|
||||||
reply.Ok = false
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[hevring] Rán gave us /w o r k/! %v\n", task)
|
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 {
|
func (h *Hevring) Stop(x int, reply *FlutAck) error {
|
||||||
if (h.task != FlutTask{}) {
|
if h.taskQuit != nil {
|
||||||
fmt.Println("[hevring] stopping task")
|
fmt.Println("[hevring] stopping task")
|
||||||
h.task = FlutTask{}
|
h.task = FlutTask{}
|
||||||
close(h.taskQuit)
|
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 {
|
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)
|
time.Sleep(100 * time.Millisecond)
|
||||||
fmt.Println("[hevring] Rán disconnected, stopping")
|
fmt.Println("[hevring] Rán disconnected, stopping")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
102
rpc/ran.go
102
rpc/ran.go
|
@ -1,20 +1,19 @@
|
||||||
package rpc
|
package rpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/rpc"
|
"net/rpc"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SpeckiJ/Hochwasser/pixelflut"
|
"github.com/SpeckiJ/Hochwasser/pixelflut"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Rán represents the RPC hub, used to coordinate `Hevring` clients.
|
||||||
|
// Implements `Fluter`
|
||||||
type Rán struct {
|
type Rán struct {
|
||||||
clients []*rpc.Client
|
clients []*rpc.Client
|
||||||
task FlutTask
|
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 RunREPL(r)
|
||||||
go func() {
|
go r.killClients(stopChan, wg)
|
||||||
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()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTask assigns a FlutTask to Rán, distributing it to all clients
|
func (r *Rán) getTask() FlutTask { return r.task }
|
||||||
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.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 {
|
for _, c := range r.clients {
|
||||||
ack := FlutAck{}
|
ack := FlutAck{}
|
||||||
// @speed: should send tasks async
|
// @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})
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue