home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

lol cursor is multiplayer now

+360 -105
+189
cursor/cursor.go
··· 1 + package cursor 2 + 3 + import ( 4 + "arimelody-web/model" 5 + "fmt" 6 + "math/rand" 7 + "net/http" 8 + "strconv" 9 + "strings" 10 + "sync" 11 + "time" 12 + 13 + "github.com/gorilla/websocket" 14 + ) 15 + 16 + type CursorClient struct { 17 + ID int32 18 + Conn *websocket.Conn 19 + Route string 20 + X int16 21 + Y int16 22 + Disconnected bool 23 + } 24 + 25 + type CursorMessage struct { 26 + Data []byte 27 + Route string 28 + Exclude []*CursorClient 29 + } 30 + 31 + func (client *CursorClient) Send(data []byte) { 32 + err := client.Conn.WriteMessage(websocket.TextMessage, data) 33 + if err != nil { 34 + client.Disconnect() 35 + } 36 + } 37 + 38 + func (client *CursorClient) Disconnect() { 39 + client.Disconnected = true 40 + broadcast <- CursorMessage{ 41 + []byte(fmt.Sprintf("leave:%d", client.ID)), 42 + client.Route, 43 + []*CursorClient{}, 44 + } 45 + } 46 + 47 + var clients = make(map[int32]*CursorClient) 48 + var broadcast = make(chan CursorMessage) 49 + var mutex = &sync.Mutex{} 50 + 51 + func StartCursor(app *model.AppState) { 52 + var includes = func (clients []*CursorClient, client *CursorClient) bool { 53 + for _, c := range clients { 54 + if c.ID == client.ID { return true } 55 + } 56 + return false 57 + } 58 + 59 + log("Cursor message handler ready!") 60 + 61 + for { 62 + message := <-broadcast 63 + mutex.Lock() 64 + for _, client := range clients { 65 + if client.Route != message.Route { continue } 66 + if includes(message.Exclude, client) { continue } 67 + client.Send(message.Data) 68 + } 69 + mutex.Unlock() 70 + } 71 + } 72 + 73 + func handleClient(client *CursorClient) { 74 + msgType, message, err := client.Conn.ReadMessage() 75 + if err != nil { 76 + client.Disconnect() 77 + return 78 + } 79 + if msgType != websocket.TextMessage { return } 80 + 81 + args := strings.Split(string(message), ":") 82 + if len(args) == 0 { return } 83 + switch args[0] { 84 + case "loc": 85 + if len(args) < 2 { return } 86 + 87 + client.Route = args[1] 88 + 89 + mutex.Lock() 90 + for _, otherClient := range clients { 91 + if otherClient.ID == client.ID { continue } 92 + if otherClient.Route != client.Route { continue } 93 + client.Send([]byte(fmt.Sprintf("join:%d", otherClient.ID))) 94 + client.Send([]byte(fmt.Sprintf("pos:%d:%d:%d", otherClient.ID, otherClient.X, otherClient.Y))) 95 + } 96 + mutex.Unlock() 97 + broadcast <- CursorMessage{ 98 + []byte(fmt.Sprintf("join:%d", client.ID)), 99 + client.Route, 100 + []*CursorClient{ client }, 101 + } 102 + case "char": 103 + if len(args) < 2 { return } 104 + // haha, turns out using ':' as a separator means you can't type ':'s 105 + // i should really be writing byte packets, not this nonsense 106 + msg := byte(':') 107 + if len(args[1]) > 0 { 108 + msg = args[1][0] 109 + } 110 + broadcast <- CursorMessage{ 111 + []byte(fmt.Sprintf("char:%d:%c", client.ID, msg)), 112 + client.Route, 113 + []*CursorClient{ client }, 114 + } 115 + case "nochar": 116 + broadcast <- CursorMessage{ 117 + []byte(fmt.Sprintf("nochar:%d", client.ID)), 118 + client.Route, 119 + []*CursorClient{ client }, 120 + } 121 + case "pos": 122 + if len(args) < 3 { return } 123 + x, err := strconv.ParseInt(args[1], 10, 32) 124 + y, err := strconv.ParseInt(args[2], 10, 32) 125 + if err != nil { return } 126 + client.X = int16(x) 127 + client.Y = int16(y) 128 + broadcast <- CursorMessage{ 129 + []byte(fmt.Sprintf("pos:%d:%d:%d", client.ID, client.X, client.Y)), 130 + client.Route, 131 + []*CursorClient{ client }, 132 + } 133 + } 134 + } 135 + 136 + func Handler(app *model.AppState) http.HandlerFunc { 137 + var upgrader = websocket.Upgrader{ 138 + CheckOrigin: func (r *http.Request) bool { 139 + origin := r.Header.Get("Origin") 140 + return origin == app.Config.BaseUrl 141 + }, 142 + } 143 + 144 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 + conn, err := upgrader.Upgrade(w, r, nil) 146 + if err != nil { 147 + log("Failed to upgrade to WebSocket connection: %v\n", err) 148 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 149 + return 150 + } 151 + defer conn.Close() 152 + 153 + client := CursorClient{ 154 + ID: rand.Int31(), 155 + Conn: conn, 156 + X: 0.0, 157 + Y: 0.0, 158 + Disconnected: false, 159 + } 160 + 161 + err = client.Conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("id:%d", client.ID))) 162 + if err != nil { 163 + client.Conn.Close() 164 + return 165 + } 166 + 167 + mutex.Lock() 168 + clients[client.ID] = &client 169 + mutex.Unlock() 170 + 171 + // log("Client connected: %s (%s)", fmt.Sprintf("0x%08x", client.ID), client.Conn.RemoteAddr().String()) 172 + 173 + for { 174 + if client.Disconnected { 175 + mutex.Lock() 176 + delete(clients, client.ID) 177 + client.Conn.Close() 178 + mutex.Unlock() 179 + return 180 + } 181 + handleClient(&client) 182 + } 183 + }) 184 + } 185 + 186 + func log(format string, args ...any) { 187 + logString := fmt.Sprintf(format, args...) 188 + fmt.Printf("[%s] [CURSOR] %s\n", time.Now().Format(time.UnixDate), logString) 189 + }
+1
go.mod
··· 10 10 require golang.org/x/crypto v0.27.0 // indirect 11 11 12 12 require ( 13 + github.com/gorilla/websocket v1.5.3 // indirect 13 14 github.com/pelletier/go-toml/v2 v2.2.3 // indirect 14 15 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect 15 16 )
+2
go.sum
··· 2 2 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 3 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 4 4 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 5 + github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 6 + github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 7 github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 6 8 github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 7 9 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+1
log/log.go
··· 30 30 TYPE_ARTWORK string = "artwork" 31 31 TYPE_FILES string = "files" 32 32 TYPE_MISC string = "misc" 33 + TYPE_CURSOR string = "cursor" 33 34 ) 34 35 35 36 type LogLevel int
+15 -1
main.go
··· 1 1 package main 2 2 3 3 import ( 4 + "bufio" 4 5 "errors" 5 6 "fmt" 6 7 stdLog "log" 7 8 "math" 8 9 "math/rand" 10 + "net" 9 11 "net/http" 10 12 "os" 11 13 "path/filepath" ··· 17 19 "arimelody-web/api" 18 20 "arimelody-web/colour" 19 21 "arimelody-web/controller" 22 + "arimelody-web/cursor" 23 + "arimelody-web/log" 20 24 "arimelody-web/model" 21 25 "arimelody-web/templates" 22 - "arimelody-web/log" 23 26 "arimelody-web/view" 24 27 25 28 "github.com/jmoiron/sqlx" ··· 428 431 os.Exit(1) 429 432 } 430 433 434 + go cursor.StartCursor(&app) 435 + 431 436 // start the web server! 432 437 mux := createServeMux(&app) 433 438 fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port) ··· 444 449 mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app))) 445 450 mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app))) 446 451 mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads")))) 452 + mux.Handle("/cursor-ws", cursor.Handler(app)) 447 453 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 448 454 if r.Method == http.MethodHead { 449 455 w.WriteHeader(http.StatusOK) ··· 534 540 type LoggingResponseWriter struct { 535 541 http.ResponseWriter 536 542 Status int 543 + } 544 + 545 + func (lrw *LoggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 546 + hijack, ok := lrw.ResponseWriter.(http.Hijacker) 547 + if !ok { 548 + return nil, nil, errors.New("Server does not support hijacking\n") 549 + } 550 + return hijack.Hijack() 537 551 } 538 552 539 553 func (lrw *LoggingResponseWriter) WriteHeader(status int) {
+148 -101
public/script/cursor.js
··· 2 2 3 3 const CURSOR_TICK_RATE = 1000/30; 4 4 const CURSOR_LERP_RATE = 1/100; 5 + const CURSOR_FUNCHAR_RATE = 20; 5 6 const CURSOR_CHAR_MAX_LIFE = 5000; 6 - const CURSOR_MAX_CHARS = 50; 7 + const CURSOR_MAX_CHARS = 64; 7 8 8 9 /** @type HTMLElement */ 9 10 let cursorContainer; ··· 11 12 let myCursor; 12 13 /** @type Map<number, Cursor> */ 13 14 let cursors = new Map(); 14 - /** @type Array<FunChar> */ 15 - let chars = new Array(); 15 + 16 + /** @type WebSocket */ 17 + let ws; 16 18 17 19 let running = false; 18 - let lastCursorUpdateTime = 0; 19 - let lastCharUpdateTime = 0; 20 + let lastUpdate = 0; 20 21 21 22 class Cursor { 22 - /** @type number */ id; 23 - 24 - // real coordinates (canonical) 25 - /** @type number */ x; 26 - /** @type number */ y; 27 - 28 - // update coordinates (interpolated) 29 - /** @type number */ rx; 30 - /** @type number */ ry; 31 - 32 23 /** @type HTMLElement */ 33 24 #element; 34 25 /** @type HTMLElement */ 35 - #char; 26 + #charElement; 27 + #funCharCooldown = CURSOR_FUNCHAR_RATE; 36 28 37 29 /** 38 - * @param {number} id 30 + * @param {string} id 39 31 * @param {number} x 40 32 * @param {number} y 41 33 */ 42 34 constructor(id, x, y) { 43 - this.id = id; 35 + // real coordinates (canonical) 44 36 this.x = x; 45 37 this.y = y; 38 + // render coordinates (interpolated) 46 39 this.rx = x; 47 40 this.ry = y; 41 + this.msg = ''; 42 + this.funChars = new Array(); 48 43 49 44 const element = document.createElement('i'); 50 45 element.classList.add('cursor'); 51 - element.id = 'cursor' + id; 52 46 const colour = randomColour(); 53 47 element.style.borderColor = colour; 54 48 element.style.color = colour; 55 - element.innerText = '0x' + navigator.userAgent.hashCode(); 49 + cursorContainer.appendChild(element); 50 + this.#element = element; 56 51 57 - const char = document.createElement('p'); 52 + const char = document.createElement('i'); 58 53 char.className = 'char'; 59 - element.appendChild(char); 54 + cursorContainer.appendChild(char); 55 + this.#charElement = char; 60 56 61 - this.#element = element; 62 - this.#char = char; 63 - cursorContainer.appendChild(this.#element); 57 + this.setID(id); 64 58 } 65 59 66 60 destroy() { 67 61 this.#element.remove(); 68 - this.#char.remove(); 62 + this.#charElement.remove(); 63 + this.funChars.forEach(char => { 64 + char.destroy(); 65 + }); 69 66 } 70 67 71 68 /** ··· 85 82 this.ry += (this.y - this.ry) * CURSOR_LERP_RATE * deltaTime; 86 83 this.#element.style.left = this.rx + 'px'; 87 84 this.#element.style.top = this.ry + 'px'; 85 + this.#charElement.style.left = this.rx + 'px'; 86 + this.#charElement.style.top = (this.ry - 24) + 'px'; 87 + 88 + if (this.#funCharCooldown > 0) 89 + this.#funCharCooldown -= deltaTime; 90 + 91 + if (this.msg.length > 0) { 92 + if (config.cursorFunMode === true) { 93 + if (this.#funCharCooldown <= 0) { 94 + this.#funCharCooldown = CURSOR_FUNCHAR_RATE; 95 + if (this.funChars.length >= CURSOR_MAX_CHARS) { 96 + const char = this.funChars.shift(); 97 + char.destroy(); 98 + } 99 + const yOffset = -20; 100 + const accelMultiplier = 0.002; 101 + this.funChars.push(new FunChar( 102 + this.x + window.scrollX, this.y + window.scrollY + yOffset, 103 + (this.x - this.rx) * accelMultiplier, (this.y - this.ry) * accelMultiplier, 104 + this.msg)); 105 + } 106 + } else { 107 + this.#charElement.innerText = this.msg; 108 + } 109 + } else { 110 + this.#charElement.innerText = ''; 111 + } 112 + 113 + this.funChars.forEach(char => { 114 + if (char.life > CURSOR_CHAR_MAX_LIFE || 115 + char.y > document.body.clientHeight || 116 + char.x < 0 || 117 + char.x > document.body.clientWidth 118 + ) { 119 + this.funChars = this.funChars.filter(c => c !== this); 120 + char.destroy(); 121 + return; 122 + } 123 + char.update(deltaTime); 124 + }); 88 125 } 89 126 90 - /** 91 - * @param {string} text 92 - */ 93 - print(text) { 94 - if (text.length > 1) return; 95 - this.#char.innerText = text; 127 + setID(id) { 128 + this.id = id; 129 + this.#element.id = 'cursor' + id; 130 + this.#element.innerText = '0x' + id.toString(16).slice(0, 8).padStart(8, '0'); 96 131 } 97 132 98 133 /** ··· 107 142 } 108 143 109 144 class FunChar { 110 - /** @type number */ x; y; 111 - /** @type number */ xa; ya; 112 - /** @type number */ r; ra; 113 - /** @type HTMLElement */ element; 114 - /** @type number */ life; 115 - 116 145 /** 117 146 * @param {number} x 118 147 * @param {number} y ··· 144 173 */ 145 174 update(deltaTime) { 146 175 this.life += deltaTime; 147 - if (this.life > CURSOR_CHAR_MAX_LIFE || 148 - this.y > document.body.clientHeight || 149 - this.x < 0 || 150 - this.x > document.body.clientWidth 151 - ) { 152 - this.destroy(); 153 - return; 154 - } 155 176 156 177 this.x += this.xa * deltaTime; 157 178 this.y += this.ya * deltaTime; ··· 164 185 } 165 186 166 187 destroy() { 167 - chars = chars.filter(char => char !== this); 168 188 this.element.remove(); 169 189 } 170 190 } 171 - 172 - String.prototype.hashCode = function() { 173 - var hash = 0; 174 - if (this.length === 0) return hash; 175 - for (let i = 0; i < this.length; i++) { 176 - const chr = this.charCodeAt(i); 177 - hash = ((hash << 5) - hash) + chr; 178 - hash |= 0; // convert to 32-bit integer 179 - } 180 - return Math.round(Math.abs(hash)).toString(16).slice(0, 8).padStart(8, '0'); 181 - }; 182 191 183 192 /** 184 193 * @returns string ··· 198 207 */ 199 208 function handleMouseMove(event) { 200 209 if (!myCursor) return; 210 + if (ws && ws.readyState == WebSocket.OPEN) 211 + ws.send(`pos:${event.x}:${event.y}`); 201 212 myCursor.move(event.x, event.y); 202 213 } 203 214 ··· 211 222 /** 212 223 * @param {KeyboardEvent} event 213 224 */ 214 - function handleKeyDown(event) { 225 + function handleKeyPress(event) { 215 226 if (event.key.length > 1) return; 216 227 if (event.metaKey || event.ctrlKey) return; 217 - if (config.cursorFunMode === true) { 218 - const yOffset = -20; 219 - const accelMultiplier = 0.002; 220 - if (chars.length < CURSOR_MAX_CHARS) 221 - chars.push(new FunChar( 222 - myCursor.x + window.scrollX, myCursor.y + window.scrollY + yOffset, 223 - (myCursor.x - myCursor.rx) * accelMultiplier, (myCursor.y - myCursor.ry) * accelMultiplier, 224 - event.key)); 225 - } else { 226 - myCursor.print(event.key); 227 - } 228 + if (myCursor.msg === event.key) return; 229 + if (ws && ws.readyState == WebSocket.OPEN) 230 + ws.send(`char:${event.key}`); 231 + myCursor.msg = event.key; 228 232 } 229 233 230 234 function handleKeyUp() { 231 - if (!config.cursorFunMode) { 232 - myCursor.print(''); 233 - } 235 + if (ws && ws.readyState == WebSocket.OPEN) 236 + ws.send(`nochar`); 237 + myCursor.msg = ''; 234 238 } 235 239 236 240 /** ··· 239 243 function updateCursors(time) { 240 244 if (!running) return; 241 245 242 - const deltaTime = time - lastCursorUpdateTime; 246 + const deltaTime = time - lastUpdate; 243 247 244 248 cursors.forEach(cursor => { 245 249 cursor.update(deltaTime); 246 250 }); 247 251 248 - lastCursorUpdateTime = time; 252 + lastUpdate = time; 249 253 requestAnimationFrame(updateCursors); 250 254 } 251 255 252 - /** 253 - * @param {number} time 254 - */ 255 - function updateChars(time) { 256 - if (!running) return; 257 - 258 - const deltaTime = time - lastCharUpdateTime; 259 - 260 - chars.forEach(char => { 261 - char.update(deltaTime); 262 - }); 263 - 264 - lastCharUpdateTime = time; 265 - requestAnimationFrame(updateChars); 266 - } 267 - 268 256 function cursorSetup() { 269 257 if (running) throw new Error('Only one instance of Cursor can run at a time.'); 270 258 running = true; ··· 273 261 cursorContainer.id = 'cursors'; 274 262 document.body.appendChild(cursorContainer); 275 263 276 - myCursor = new Cursor(0, window.innerWidth / 2, window.innerHeight / 2); 264 + myCursor = new Cursor("You!", window.innerWidth / 2, window.innerHeight / 2); 277 265 cursors.set(0, myCursor); 278 266 279 267 document.addEventListener('mousemove', handleMouseMove); 280 268 document.addEventListener('mousedown', handleMouseDown); 281 269 document.addEventListener('mouseup', handleMouseUp); 282 - document.addEventListener('keydown', handleKeyDown); 270 + document.addEventListener('keypress', handleKeyPress); 283 271 document.addEventListener('keyup', handleKeyUp); 284 272 285 273 requestAnimationFrame(updateCursors); 286 - requestAnimationFrame(updateChars); 274 + 275 + ws = new WebSocket("/cursor-ws"); 276 + 277 + ws.addEventListener("open", () => { 278 + console.log("Cursor connected to server successfully."); 287 279 288 - console.debug(`Cursor tracking @ ${window.location.pathname}`); 280 + ws.send(`loc:${window.location.pathname}`); 281 + }); 282 + ws.addEventListener("error", error => { 283 + console.error("Cursor WebSocket error:", error); 284 + }); 285 + ws.addEventListener("close", () => { 286 + console.log("Cursor connection closed."); 287 + }); 288 + 289 + ws.addEventListener("message", event => { 290 + const args = String(event.data).split(":"); 291 + if (args.length == 0) return; 292 + 293 + let id = 0; 294 + /** @type Cursor */ 295 + let cursor; 296 + if (args.length > 1) { 297 + id = Number(args[1]); 298 + cursor = cursors.get(id); 299 + } 300 + 301 + switch (args[0]) { 302 + case 'id': { 303 + myCursor.setID(Number(args[1])); 304 + break; 305 + } 306 + case 'join': { 307 + if (id === myCursor.id) break; 308 + cursors.set(id, new Cursor(id, 0, 0)); 309 + break; 310 + } 311 + case 'leave': { 312 + if (!cursor || cursor === myCursor) break; 313 + cursors.get(id).destroy(); 314 + cursors.delete(id); 315 + break; 316 + } 317 + case 'char': { 318 + if (!cursor || cursor === myCursor) break; 319 + cursor.msg = args[2]; 320 + break; 321 + } 322 + case 'nochar': { 323 + if (!cursor || cursor === myCursor) break; 324 + cursor.msg = ''; 325 + break; 326 + } 327 + case 'pos': { 328 + if (!cursor || cursor === myCursor) break; 329 + cursor.move(Number(args[2]), Number(args[3])); 330 + break; 331 + } 332 + default: { 333 + console.warn("Cursor: Unknown command received from server:", args[0]); 334 + break; 335 + } 336 + } 337 + }); 338 + 339 + console.log(`Cursor tracking @ ${window.location.pathname}`); 289 340 } 290 341 291 342 function cursorDestroy() { ··· 294 345 document.removeEventListener('mousemove', handleMouseMove); 295 346 document.removeEventListener('mousedown', handleMouseDown); 296 347 document.removeEventListener('mouseup', handleMouseUp); 297 - document.removeEventListener('keydown', handleKeyDown); 348 + document.removeEventListener('keypress', handleKeyPress); 298 349 document.removeEventListener('keyup', handleKeyUp); 299 350 300 - chars.forEach(char => { 301 - char.destroy(); 302 - }); 303 - chars = new Array(); 304 351 cursors.forEach(cursor => { 305 352 cursor.destroy(); 306 353 }); ··· 309 356 310 357 cursorContainer.remove(); 311 358 312 - console.debug(`Cursor no longer tracking.`); 359 + console.log(`Cursor no longer tracking.`); 313 360 running = false; 314 361 } 315 362
+4 -3
public/style/cursor.css
··· 20 20 border-color: var(--on-background) !important; 21 21 } 22 22 23 - #cursors i.cursor .char { 23 + #cursors i.char { 24 24 position: absolute; 25 - transform: translateY(-44px); 26 25 margin: 0; 26 + font-style: normal; 27 27 font-size: 20px; 28 + font-weight: bold; 28 29 color: var(--on-background); 29 30 } 30 31 31 - #cursors .funchar { 32 + #cursors i.funchar { 32 33 position: absolute; 33 34 margin: 0; 34 35