From 4a9b96e5cc8bf822bf3b2860b175f446cb45f98a Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Fri, 1 Jun 2018 19:33:11 +0100 Subject: Implemented animated GIFs --- Server2.go | 297 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 Server2.go (limited to 'Server2.go') diff --git a/Server2.go b/Server2.go new file mode 100644 index 0000000..2869fc4 --- /dev/null +++ b/Server2.go @@ -0,0 +1,297 @@ +package main + +import ( + "bytes" + "image" + "image/color" + "image/gif" + "os" + + "io/ioutil" + "log" + "net/http" + "path" + + "github.com/gorilla/websocket" + + "github.com/MFAshby/unicornpaint/unicorn" +) + +var ( + un unicorn.Unicorn2 + + upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(*http.Request) bool { return true }, + } + delCh = make(chan *websocket.Conn) + addCh = make(chan *websocket.Conn) + broadcastCh = make(chan interface{}) + clients = map[*websocket.Conn]bool{} +) + +// Types of commands +type commandType string + +const ( + noop commandType = "NO_OP" + setPixel commandType = "SET_PIXEL" + clear commandType = "CLEAR" + save commandType = "SAVE" + load commandType = "LOAD" + addFrame commandType = "ADD_FRAME" + removeFrame commandType = "REMOVE_FRAME" +) + +type Command struct { + Type commandType `json:"type"` + X uint8 `json:"x"` + Y uint8 `json:"y"` + R uint8 `json:"r"` + G uint8 `json:"g"` + B uint8 `json:"b"` + Frame int `json:"frame"` + Delay int `json:"delay"` + SaveName string `json:"saveName"` +} + +const ( + savesDir = "saves" +) + +type State struct { + Saves []string `json:"saves"` + ImageData []byte `json:"imageData"` +} + +func getGifBytes() ([]byte, error) { + g := un.GetGif() + buf := &bytes.Buffer{} + err := gif.EncodeAll(buf, g) + return buf.Bytes(), err +} + +func getState() *State { + infos, err := ioutil.ReadDir(savesDir) + if err != nil { + log.Printf("Error reading saves dir %v", err) + } + saveFileNames := make([]string, len(infos)) + for ix, info := range infos { + saveFileNames[ix] = info.Name() + } + + // Write out as GIF file + gifBytes, err := getGifBytes() + if err != nil { + log.Printf("Error getting GIF bytes %v", err) + } + + return &State{ + ImageData: gifBytes, + Saves: saveFileNames, + } +} + +func savePic(saveFileName string) { + f, err := os.Create(path.Join(savesDir, saveFileName)) + if err != nil { + log.Printf("Failed to open file for saving %v", err) + return + } + defer f.Close() + + g := un.GetGif() + err = gif.EncodeAll(f, g) +} + +func loadPic(saveFileName string) { + f, err := os.Open(path.Join(savesDir, saveFileName)) + if err != nil { + log.Printf("Failed to read file %v", err) + return + } + defer f.Close() + + g, err := gif.DecodeAll(f) + if err != nil { + log.Printf("Failed to decode GIF file %v", err) + return + } + + un.SetGif(g) +} + +func doSetPixel(x, y, r, g, b uint8, frame int) { + gf := un.GetGif() + if int(frame) >= len(gf.Image) { + log.Printf("Tried to set pixel in frame out of range %v", frame) + return + } + im := gf.Image[frame] + im.Set(int(x), int(y), color.RGBA{ + R: uint8(r), + G: uint8(g), + B: uint8(b), + A: uint8(255), + }) +} + +func doAddFrame(frame int, delay int) { + gf := un.GetGif() + if frame < 1 || frame > len(gf.Image) { + log.Printf("Trying to add frame in invalid position %v", frame) + return + } + + // Copy the image before it + sourceImage := gf.Image[frame-1] + + buf := &bytes.Buffer{} + if err := gif.Encode(buf, sourceImage, nil); err != nil { + log.Printf("Failed to encode image %v", err) + return + } + im, err := gif.Decode(buf) + if err != nil { + log.Printf("Failed to decode image %v", err) + return + } + + newImage, ok := im.(*image.Paletted) + if !ok { + log.Printf("Wrong image type %v", err) + } + + gf.Image = append(gf.Image[:frame], append([]*image.Paletted{newImage}, gf.Image[frame:]...)...) + gf.Delay = append(gf.Delay[:frame], append([]int{delay}, gf.Delay[frame:]...)...) + gf.Disposal = append(gf.Disposal[:frame], append([]byte{gif.DisposalBackground}, gf.Disposal[frame:]...)...) +} + +func doRemoveFrame(frame int) { + // Can't remove the first frame + gf := un.GetGif() + frameCount := len(gf.Image) + if frame < 0 || frame >= frameCount { + log.Printf("Trying to remove frame %v which is invalid", frame) + return + } + + if frameCount == 1 { + log.Printf("Trying to remove the last frame, ignoring") + return + } + + gf.Image = append(gf.Image[:frame], gf.Image[frame+1:]...) + gf.Delay = append(gf.Delay[:frame], gf.Delay[frame+1:]...) + gf.Disposal = append(gf.Disposal[:frame], gf.Disposal[frame+1:]...) +} + +func doClear(frame int) { + gf := un.GetGif() + if frame >= len(gf.Image) { + log.Printf("Trying to clear invalid frame %v", frame) + return + } + + im := gf.Image[frame] + b := im.Bounds() + width := b.Dx() + height := b.Dy() + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + im.SetColorIndex(x, y, 0) // 0 in WebSafe colors is black + } + } +} + +func upgradeHandler(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Failed to upgrade someone %v", err) + return + } + + // Add you to my list, remember to remove later + addCh <- conn + defer func() { delCh <- conn }() + + // Get you up to speed + err = conn.WriteJSON(getState()) + if err != nil { + log.Printf("Failed to send initial state %v", err) + return + } + + // Read & execute commands in a loop until error + for { + cmd := Command{} + err = conn.ReadJSON(&cmd) + if err != nil { + log.Printf("Error reading from client %v", err) + break + } + + switch cmd.Type { + case noop: + // Don't do anything + case setPixel: + doSetPixel(cmd.X, cmd.Y, cmd.R, cmd.G, cmd.B, cmd.Frame) + case clear: + doClear(cmd.Frame) + case save: + savePic(cmd.SaveName) + case load: + loadPic(cmd.SaveName) + case addFrame: + doAddFrame(cmd.Frame, cmd.Delay) + case removeFrame: + doRemoveFrame(cmd.Frame) + } + // Pretty much all commands demand a change of state, + // do a broadcast each time + broadcastCh <- getState() + } +} + +func handleClients() { + for { + select { + case c := <-addCh: + clients[c] = true + case c := <-delCh: + delete(clients, c) + case msg := <-broadcastCh: + doBroadcast(msg) + } + } +} + +func main() { + uni, err := unicorn.NewUnicorn2() + if err != nil { + log.Fatalf("Couldn't get a unicorn :( %v", err) + } + un = uni + un.StartRender() + + log.Println("Starting server on port 3001") + http.Handle("/", http.FileServer(http.Dir("build"))) + http.HandleFunc("/ws", upgradeHandler) + go http.ListenAndServe(":3001", nil) + go handleClients() + un.MainLoop() +} + +func doBroadcast(obj interface{}) { + for client := range clients { + err := client.WriteJSON(obj) + + if err != nil { + log.Printf("Error writing to client, closing %v", err) + client.Close() + delete(clients, client) + } + } +} -- cgit v1.2.3-ZIG