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"
|
||||
|
||||
"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 🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊
|
||||
|
|
|
@ -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 {
|
||||
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)
|
||||
|
|
102
rpc/ran.go
102
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})
|
||||
}
|
||||
|
|
|
@ -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