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) | ||||
|  |  | |||
							
								
								
									
										78
									
								
								rpc/ran.go
								
								
								
								
							
							
						
						
									
										78
									
								
								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,21 +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
 | ||||
| 				} | ||||
| 	go RunREPL(r) | ||||
| 	go r.killClients(stopChan, wg) | ||||
| 
 | ||||
| 			} else if cmd == "start" { | ||||
| 				if (r.task != FlutTask{}) { | ||||
| 	return r | ||||
| } | ||||
| 
 | ||||
| func (r *Rán) getTask() FlutTask { return r.task } | ||||
| 
 | ||||
| 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
 | ||||
|  | @ -116,38 +117,23 @@ func SummonRán(address string, stopChan chan bool, wg *sync.WaitGroup) *Rán { | |||
| 			log.Printf("[rán] client didn't accept task") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (r *Rán) stopTask() { | ||||
| 	// @robustness: errorchecking
 | ||||
| 	for _, c := range r.clients { | ||||
| 		ack := FlutAck{} | ||||
| 		c.Call("Hevring.Stop", 0, &ack) // @speed: async
 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 			} 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() { | ||||
| 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() | ||||
| 	}() | ||||
| 
 | ||||
| 	return r | ||||
| } | ||||
| 
 | ||||
| // SetTask assigns a FlutTask to Rán, distributing it to all clients
 | ||||
|  | @ -156,13 +142,5 @@ func (r *Rán) SetTask(img *image.NRGBA, offset image.Point, address string, max | |||
| 	//   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} | ||||
| 	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") | ||||
| 		} | ||||
| 	} | ||||
| 	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