new offset implementation

allows specifying a mask image for the random offset.
Offsetter interface could also be used for a random walk / DVD-bounce
incremental offset.
This commit is contained in:
Norwin 2022-01-02 11:14:11 +01:00
parent 4e0cc35c26
commit 8d6aa732d7
6 changed files with 103 additions and 26 deletions

View File

@ -75,7 +75,7 @@ func taskFromFlags(stop chan bool, wg *sync.WaitGroup) {
FlutTaskOpts: pixelflut.FlutTaskOpts{ FlutTaskOpts: pixelflut.FlutTaskOpts{
Address: *address, Address: *address,
MaxConns: *connections, MaxConns: *connections,
Offset: image.Pt(*x, *y), Offset: pixelflut.RandOffsetter{Point: image.Pt(*x, *y)},
RenderOrder: pixelflut.NewOrder(*order), RenderOrder: pixelflut.NewOrder(*order),
}, },
Img: img, Img: img,

View File

@ -45,7 +45,7 @@ func FetchImage(bounds *image.Rectangle, address string, conns int, stop chan bo
} }
go readPixels(img, conn, stop) go readPixels(img, conn, stop)
go bombConn(cmds[i], 0, 0, conn, stop) go bombConn(cmds[i], &RandOffsetter{}, conn, stop)
} }
return img return img

View File

@ -39,7 +39,7 @@ func OffsetCmd(x, y int) []byte {
} }
// CommandsFromImage converts an image to the respective pixelflut commands // CommandsFromImage converts an image to the respective pixelflut commands
func commandsFromImage(img *image.NRGBA, order RenderOrder, offset image.Point) (cmds commands) { func commandsFromImage(img *image.NRGBA, order RenderOrder, offset RandOffsetter) (cmds commands) {
b := img.Bounds() b := img.Bounds()
cmds = make([][]byte, b.Size().X*b.Size().Y) cmds = make([][]byte, b.Size().X*b.Size().Y)
numCmds := 0 numCmds := 0
@ -74,9 +74,9 @@ func commandsFromImage(img *image.NRGBA, order RenderOrder, offset image.Point)
var cmd []byte var cmd []byte
cmd = append(cmd, []byte("PX ")...) cmd = append(cmd, []byte("PX ")...)
cmd = strconv.AppendUint(cmd, uint64(x+offset.X), 10) cmd = strconv.AppendUint(cmd, uint64(x), 10)
cmd = append(cmd, ' ') cmd = append(cmd, ' ')
cmd = strconv.AppendUint(cmd, uint64(y+offset.Y), 10) cmd = strconv.AppendUint(cmd, uint64(y), 10)
cmd = append(cmd, ' ') cmd = append(cmd, ' ')
appendColor(&cmd, c) appendColor(&cmd, c)
cmd = append(cmd, '\n') cmd = append(cmd, '\n')

View File

@ -20,11 +20,10 @@ type FlutTask struct {
type FlutTaskOpts struct { type FlutTaskOpts struct {
Address string Address string
MaxConns int MaxConns int
Offset image.Point
Paused bool Paused bool
RGBSplit bool // @cleanup: replace with `FX: []Effect` RGBSplit bool // @cleanup: replace with `FX: []Effect`
RandOffset bool
RenderOrder RenderOrder RenderOrder RenderOrder
Offset RandOffsetter
} }
// FlutTaskData contains the actual pixeldata to flut, separated because of size // FlutTaskData contains the actual pixeldata to flut, separated because of size
@ -36,8 +35,8 @@ func (t FlutTask) String() string {
img = t.Img.Bounds().Size().String() img = t.Img.Bounds().Size().String()
} }
return fmt.Sprintf( return fmt.Sprintf(
" %d conns @ %s\n img %v offset %v\n order %s rgbsplit %v randoffset %v paused %v", " %d conns @ %s\n img %v offset %v\n order %s rgbsplit %v paused %v",
t.MaxConns, t.Address, img, t.Offset, t.RenderOrder, t.RGBSplit, t.RandOffset, t.Paused, t.MaxConns, t.Address, img, t.Offset, t.RenderOrder, t.RGBSplit, t.Paused,
) )
} }
@ -84,10 +83,11 @@ func Flut(t FlutTask, stop chan bool, wg *sync.WaitGroup) {
var messages [][]byte var messages [][]byte
var maxOffsetX, maxOffsetY int var maxOffsetX, maxOffsetY int
if t.RandOffset { if t.Offset.Random {
maxX, maxY := CanvasSize(t.Address) maxX, maxY := CanvasSize(t.Address)
maxOffsetX = maxX - t.Img.Bounds().Canon().Dx() maxOffsetX = maxX - t.Img.Bounds().Canon().Dx()
maxOffsetY = maxY - t.Img.Bounds().Canon().Dy() maxOffsetY = maxY - t.Img.Bounds().Canon().Dy()
t.Offset.SetMaximumOffset(image.Pt(maxOffsetX, maxOffsetY))
messages = cmds.Chunk(1) // each connection should send the full img messages = cmds.Chunk(1) // each connection should send the full img
} else { } else {
messages = cmds.Chunk(t.MaxConns) messages = cmds.Chunk(t.MaxConns)
@ -102,7 +102,7 @@ func Flut(t FlutTask, stop chan bool, wg *sync.WaitGroup) {
time.Sleep(50 * time.Millisecond) // avoid crashing the server time.Sleep(50 * time.Millisecond) // avoid crashing the server
go bombAddress(msg, t.Address, maxOffsetX, maxOffsetY, stop, &bombWg) go bombAddress(msg, t.Address, &t.Offset, stop, &bombWg)
} }
bombWg.Wait() bombWg.Wait()
if wg != nil { if wg != nil {
@ -114,6 +114,7 @@ func generateCommands(t FlutTask) (cmds commands) {
if t.RGBSplit { if t.RGBSplit {
white := color.NRGBA{0xff, 0xff, 0xff, 0xff} white := color.NRGBA{0xff, 0xff, 0xff, 0xff}
imgmod := render.ImgColorFilter(t.Img, white, color.NRGBA{0xff, 0, 0, 0xff}) imgmod := render.ImgColorFilter(t.Img, white, color.NRGBA{0xff, 0, 0, 0xff})
// FIXME: this offset is ignored with the latest changes, restore this behaviour.
cmds = append(cmds, commandsFromImage(imgmod, t.RenderOrder, t.Offset.Add(image.Pt(-10, -10)))...) cmds = append(cmds, commandsFromImage(imgmod, t.RenderOrder, t.Offset.Add(image.Pt(-10, -10)))...)
imgmod = render.ImgColorFilter(t.Img, white, color.NRGBA{0, 0xff, 0, 0xff}) imgmod = render.ImgColorFilter(t.Img, white, color.NRGBA{0, 0xff, 0, 0xff})
cmds = append(cmds, commandsFromImage(imgmod, t.RenderOrder, t.Offset.Add(image.Pt(10, 0)))...) cmds = append(cmds, commandsFromImage(imgmod, t.RenderOrder, t.Offset.Add(image.Pt(10, 0)))...)

View File

@ -2,6 +2,7 @@ package pixelflut
import ( import (
"fmt" "fmt"
"image"
"math/rand" "math/rand"
"net" "net"
"sync" "sync"
@ -13,6 +14,71 @@ const (
timeoutMax = 10 * time.Second timeoutMax = 10 * time.Second
) )
type Offsetter interface {
Next() (x, y int)
SetMaximumOffset(max image.Point)
}
type RandOffsetter struct {
image.Point
Mask *image.NRGBA
Random bool
Max image.Point
}
func (r RandOffsetter) String() string {
mask := "nil"
if r.Mask != nil {
mask = r.Mask.Bounds().Size().String()
}
return fmt.Sprintf("[%v + random %v, mask %v]", r.Point, r.Random, mask)
}
// override image.Point interface
func (r RandOffsetter) Add(p image.Point) RandOffsetter {
r.Point = r.Point.Add(p)
return r
}
func (r *RandOffsetter) SetMaximumOffset(max image.Point) {
if r.Mask != nil {
mask := r.Mask.Bounds().Canon().Max.Sub(r.Point)
r.Max.X = clamp(mask.X, 1, max.X)
r.Max.Y = clamp(mask.Y, 1, max.Y)
} else {
r.Max.X = clamp(max.X, 1, max.X)
r.Max.Y = clamp(max.Y, 1, max.Y)
}
}
func clamp(val, min, max int) int {
if val < min {
return min
}
if val > max {
return max
}
return val
}
func (r RandOffsetter) Next() (x, y int) {
if r.Random {
for i := 0; i < 1000; i++ {
x := rand.Intn(r.Max.X)
y := rand.Intn(r.Max.Y)
if r.Mask != nil {
if _, _, _, a := r.Mask.At(x, y).RGBA(); a != 0 {
return r.X + x, r.Y + y
}
} else {
return r.X + x, r.Y + y
}
}
}
return r.X, r.Y
}
// Performance contains pixelflut metrics // Performance contains pixelflut metrics
type Performance struct { type Performance struct {
Enabled bool Enabled bool
@ -94,7 +160,7 @@ func initPerfReporter() *Performance {
// bombAddress opens a TCP connection to `address`, and writes `message` repeatedly, until `stop` is closed. // bombAddress opens a TCP connection to `address`, and writes `message` repeatedly, until `stop` is closed.
// It retries with exponential backoff on network errors. // It retries with exponential backoff on network errors.
func bombAddress(message []byte, address string, maxOffsetX, maxOffsetY int, stop chan bool, wg *sync.WaitGroup) { func bombAddress(message []byte, address string, offsetter Offsetter, stop chan bool, wg *sync.WaitGroup) {
wg.Add(1) wg.Add(1)
defer wg.Done() defer wg.Done()
@ -115,7 +181,7 @@ func bombAddress(message []byte, address string, maxOffsetX, maxOffsetY int, sto
fmt.Printf("[net] bombing %s with new connection\n", address) fmt.Printf("[net] bombing %s with new connection\n", address)
err = bombConn(message, maxOffsetX, maxOffsetY, conn, stop) err = bombConn(message, offsetter, conn, stop)
conn.Close() conn.Close()
timeout = timeoutMin timeout = timeoutMin
if err == nil { if err == nil {
@ -127,25 +193,25 @@ func bombAddress(message []byte, address string, maxOffsetX, maxOffsetY int, sto
// bombConn writes the given message to the given connection in a tight loop, until `stop` is closed. // bombConn writes the given message to the given connection in a tight loop, until `stop` is closed.
// Does no transformation on the given message, so make sure packet splitting / nagle works. // Does no transformation on the given message, so make sure packet splitting / nagle works.
func bombConn(message []byte, maxOffsetX, maxOffsetY int, conn net.Conn, stop chan bool) error { func bombConn(message []byte, offsetter Offsetter, conn net.Conn, stop chan bool) error {
PerformanceReporter.connsReporter <- 1 PerformanceReporter.connsReporter <- 1
defer func() { PerformanceReporter.connsReporter <- -1 }() defer func() { PerformanceReporter.connsReporter <- -1 }()
var msg = make([]byte, len(message)+16) // leave some space for offset cmd var msg = make([]byte, len(message)+16) // leave some space for offset cmd
msg = message msg = message
randOffset := maxOffsetX > 0 && maxOffsetY > 0 // randOffset := maxOffsetX > 0 && maxOffsetY > 0
for { for {
select { select {
case <-stop: case <-stop:
return nil return nil
default: default:
if randOffset { // if randOffset {
msg = append( msg = append(
OffsetCmd(rand.Intn(maxOffsetX), rand.Intn(maxOffsetY)), OffsetCmd(offsetter.Next()),
message..., message...,
) )
} // }
b, err := conn.Write(msg) b, err := conn.Write(msg)
if err != nil { if err != nil {
return err return err

View File

@ -67,15 +67,25 @@ func RunREPL(f Fluter) {
t.Paused = false t.Paused = false
case "offset", "of": case "offset", "of":
if len(args) == 1 && args[0] == "rand" { if len(args) >= 1 && args[0][0] == 'r' {
t.RandOffset = true t.Offset.Random = true
t.Offset = image.Point{} t.Offset.Mask = nil
if len(args) >= 2 {
fmt.Println(strings.Join(args[1:], " "))
mask, err := render.ReadImage(strings.Join(args[1:], " "))
if err != nil {
fmt.Printf("couldn't read mask image: %s\n", err)
continue
}
t.Offset.Mask = mask
}
} else if len(args) == 2 { } else if len(args) == 2 {
t.RandOffset = false t.Offset.Random = false
t.Offset.Mask = nil
x, err := strconv.Atoi(args[0]) x, err := strconv.Atoi(args[0])
y, err2 := strconv.Atoi(args[1]) y, err2 := strconv.Atoi(args[1])
if err == nil && err2 == nil { if err == nil && err2 == nil {
t.Offset = image.Pt(x, y) t.Offset.Point = image.Pt(x, y)
} }
} }