├── .gitignore ├── LICENSE ├── README.md ├── canvas.go ├── cmd.go ├── demo.gif ├── files ├── frontend.js └── index.html ├── frontend_js.go ├── index_html.go ├── limiter.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | pocketplace 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ola Holmström 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pocketplace 2 | 3 | > :fireworks: Draw pixels on a canvas with friends. 4 | 5 |  6 | 7 | * [x] Completely in-memory, no need for a database. 8 | * [x] Statically linked, everything you need in one binary, including the frontend. 9 | 10 | # Install 11 | 12 | ```bash 13 | $ go get github.com/olahol/pocketplace 14 | ``` 15 | 16 | # Example 17 | 18 | ```bash 19 | $ pocketplace -port 8080 -size 200 -cooldown 0 20 | Canvas pixel size 200x200 21 | Drawing cooldown 0s 22 | Listening on port 8080 23 | ``` 24 | 25 | # Development 26 | 27 | To build the frontend into the binary I use [`file2const`](https://github.com/bouk/file2const) 28 | and `go:generate` directives. So if you are modifying the frontend don't forget to: 29 | 30 | ```bash 31 | $ go generate 32 | $ go build 33 | ``` 34 | 35 | to build the binary correctly with the updated frontend. 36 | -------------------------------------------------------------------------------- /canvas.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Canvas struct { 8 | Size int 9 | Data []byte 10 | RWMutex *sync.RWMutex 11 | } 12 | 13 | func NewCanvas(size int) *Canvas { 14 | bi := &Canvas{ 15 | Size: size, 16 | Data: make([]byte, size*size*3), 17 | RWMutex: &sync.RWMutex{}, 18 | } 19 | 20 | for i, _ := range bi.Data { 21 | bi.Data[i] = 0 22 | } 23 | 24 | return bi 25 | } 26 | 27 | func (c *Canvas) Set(x, y int, r, g, b byte) error { 28 | j := (y*c.Size + x) * 3 29 | c.Data[j] = r 30 | c.Data[j+1] = g 31 | c.Data[j+2] = b 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type Cmd struct { 10 | x, y int 11 | r, g, b byte 12 | } 13 | 14 | func ParseCmd(size int, msg []byte) (Cmd, error) { 15 | str := string(msg) 16 | parts := strings.Split(str, " ") 17 | cmd := Cmd{} 18 | 19 | if len(parts) != 5 { 20 | return cmd, errors.New("should be of the format 'x y r g b'") 21 | } 22 | 23 | args := make([]int, 5) 24 | 25 | for i, p := range parts { 26 | a, err := strconv.Atoi(p) 27 | 28 | if err != nil { 29 | return cmd, err 30 | } 31 | 32 | if a < 0 { 33 | return cmd, errors.New("no arguments should be negative") 34 | } 35 | 36 | if i < 2 && a >= size { 37 | return cmd, errors.New("x, y should not be larger than size of canvas") 38 | } 39 | 40 | if i > 1 && a > 255 { 41 | return cmd, errors.New("r, g, b should not be larger than 255") 42 | } 43 | 44 | args[i] = a 45 | } 46 | 47 | cmd.x = args[0] 48 | cmd.y = args[1] 49 | cmd.r = byte(args[2]) 50 | cmd.g = byte(args[3]) 51 | cmd.b = byte(args[4]) 52 | 53 | return cmd, nil 54 | } 55 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olahol/pocketplace/661c5a9c209c416f0d99b97e74f20307698ef7c6/demo.gif -------------------------------------------------------------------------------- /files/frontend.js: -------------------------------------------------------------------------------- 1 | // Globals 2 | var SCALE = 20; 3 | 4 | var CANVAS = null; 5 | var LOADING = null; 6 | var PICKER = null; 7 | 8 | var CONTEXT = null; 9 | var CONNECTION = null; 10 | 11 | var MY_COLOR = {r: 255, g: 0, b: 0}; 12 | var ON_COOLDOWN = false; 13 | 14 | // Initialize 15 | window.onload = function () { 16 | CANVAS = document.getElementById("canvas"); 17 | LOADING = document.getElementById("loading"); 18 | PICKER = document.getElementById("picker"); 19 | 20 | setupCanvas(); 21 | setupWebsocket(); 22 | setupPicker(); 23 | }; 24 | 25 | function setupCanvas () { 26 | CONTEXT = CANVAS.getContext("2d"); 27 | 28 | CANVAS.width = SIZE; 29 | CANVAS.height = SIZE; 30 | CANVAS.style.transform = "scale(" + SCALE + ")"; 31 | 32 | CANVAS.onclick = function (e) { 33 | var x = Math.floor(e.pageX / SCALE); 34 | var y = Math.floor(e.pageY / SCALE); 35 | if (!ON_COOLDOWN) { 36 | sendPixel(x, y, MY_COLOR.r, MY_COLOR.g, MY_COLOR.b); 37 | toggleCooldown(); 38 | } 39 | }; 40 | 41 | CANVAS.oncontextmenu = function (e) { 42 | e.preventDefault(); 43 | }; 44 | } 45 | 46 | function toggleCooldown () { 47 | if (ON_COOLDOWN) { return; } 48 | if (COOLDOWN <= 0) { return; } 49 | 50 | ON_COOLDOWN = true; 51 | 52 | var el = document.getElementById("cooldown"); 53 | 54 | var cooldownMs = COOLDOWN * 1000; 55 | var cooldownEnd = Date.now() + cooldownMs; 56 | 57 | el.style.display = "block"; 58 | el.textContent = cooldownMs + "ms"; 59 | 60 | var intervalId = setInterval(() => { 61 | el.textContent = (cooldownEnd - Date.now()) + "ms"; 62 | }, 5); 63 | 64 | setTimeout(() => { 65 | ON_COOLDOWN = false; 66 | clearInterval(intervalId); 67 | el.style.display = "none"; 68 | }, cooldownMs); 69 | } 70 | 71 | function setupWebsocket () { 72 | function processCmd (data) { 73 | var parts = data.split(" "); 74 | if (parts.length === 5) { 75 | setPixel(parts[0], parts[1], parts[2], parts[3], parts[4]); 76 | } 77 | } 78 | 79 | function fillCanvas (data) { 80 | var dv = new DataView(data); 81 | var image = CONTEXT.createImageData(SIZE, SIZE); 82 | 83 | var i, j, k; 84 | for (i = 0; i < (dv.byteLength / 3); i += 1) { 85 | j = i*3; 86 | k = i*4; 87 | image.data[k] = dv.getUint8(j); 88 | image.data[k+1] = dv.getUint8(j+1); 89 | image.data[k+2] = dv.getUint8(j+2); 90 | image.data[k+3] = 255; 91 | } 92 | 93 | CONTEXT.putImageData(image, 0, 0); 94 | 95 | LOADING.style.display = "none"; 96 | CANVAS.style.display = "block"; 97 | PICKER.style.display = "block"; 98 | } 99 | 100 | var url = "ws://" + window.location.host + "/ws"; 101 | 102 | CONNECTION = new WebSocket(url); 103 | CONNECTION.binaryType = "arraybuffer"; 104 | 105 | CONNECTION.onmessage = function (e) { 106 | var data = e.data; 107 | 108 | if (typeof data === "string") { 109 | return processCmd(data); 110 | } 111 | 112 | if (data instanceof ArrayBuffer) { 113 | return fillCanvas(data); 114 | } 115 | }; 116 | } 117 | 118 | function setupPicker () { 119 | var input = document.getElementById("picker-input"); 120 | var preview = document.getElementById("picker-preview"); 121 | 122 | preview.style.backgroundColor = "#FF0000"; 123 | input.value = "#FF0000"; 124 | 125 | input.onchange = function (e) { 126 | var value = e.target.value; 127 | var color = hexToRgb(value); 128 | 129 | if (color) { 130 | MY_COLOR = color; 131 | preview.style.backgroundColor = value; 132 | } 133 | }; 134 | } 135 | 136 | // Canvas methods. 137 | function sendPixel (x, y, r, g, b) { 138 | CONNECTION.send([x, y, r, g, b].join(" ")); 139 | } 140 | 141 | function setPixel (x, y, r, g, b) { 142 | if (!this.id) { 143 | this.id = CONTEXT.createImageData(1, 1); 144 | this.idd = this.id.data; 145 | this.idd[3] = 255; 146 | } 147 | 148 | this.idd[0] = r; 149 | this.idd[1] = g; 150 | this.idd[2] = b; 151 | 152 | CONTEXT.putImageData(this.id, x, y); 153 | } 154 | 155 | // Util 156 | function hexToRgb (hex) { 157 | var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 158 | 159 | hex = hex.replace(shorthandRegex, function(m, r, g, b) { 160 | return r + r + g + g + b + b; 161 | }); 162 | 163 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 164 | 165 | return result ? { 166 | r: parseInt(result[1], 16), 167 | g: parseInt(result[2], 16), 168 | b: parseInt(result[3], 16) 169 | } : null; 170 | } 171 | -------------------------------------------------------------------------------- /files/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |