Skip to content

Instantly share code, notes, and snippets.

@wybiral
Last active September 11, 2023 08:53
Show Gist options
  • Save wybiral/c8f46fdf1fc558d631b55de3a0267771 to your computer and use it in GitHub Desktop.
Save wybiral/c8f46fdf1fc558d631b55de3a0267771 to your computer and use it in GitHub Desktop.
Tracking cursor position in real-time with remote monitoring (without JavaScript)
// Tracking cursor position in real-time without JavaScript
// Demo: https://twitter.com/davywtf/status/1124146339259002881
package main
import (
"fmt"
"net/http"
"strings"
)
const W = 50
const H = 50
var ch chan string
const head = `<head>
<style>
*{margin:0;padding:0}
html,body{width:100%;height:100%}
p{
width:10px;
height:10px;
display:inline-block;
border-right:1px solid #666;
border-bottom:1px solid #666
}
</style>
</head>`
func main() {
ch = make(chan string)
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
if p == "/" {
index(w, r)
return
} else if p == "/watch" {
watch(w, r)
return
} else {
if strings.HasPrefix(p, "/c") && strings.HasSuffix(p, ".png") {
ch <- p[1 : len(p)-4]
}
}
}
func index(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
flusher, ok := w.(http.Flusher)
if !ok {
return
}
w.Write([]byte(head))
flusher.Flush()
// Send <p> grid
w.Write([]byte("<body>\n"))
for i := 0; i < H; i++ {
w.Write([]byte("<div>"))
for j := 0; j < W; j++ {
w.Write([]byte(fmt.Sprintf("<p id=\"c%dx%d\"></p>", i, j)))
}
w.Write([]byte("</div>\n"))
}
w.Write([]byte("</body>\n"))
flusher.Flush()
// Send CSS
w.Write([]byte("<style>"))
for i := 0; i < H; i++ {
for j := 0; j < W; j++ {
id := fmt.Sprintf("c%dx%d", i, j)
s := fmt.Sprintf("#%s:hover{background:url(\"%s.png\")}", id, id)
w.Write([]byte(s))
}
}
w.Write([]byte("</style>"))
flusher.Flush()
}
func watch(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
flusher, ok := w.(http.Flusher)
if !ok {
return
}
w.Write([]byte(head))
flusher.Flush()
// Send <p> grid
w.Write([]byte("<body>\n"))
for i := 0; i < H; i++ {
w.Write([]byte("<div>"))
for j := 0; j < W; j++ {
w.Write([]byte(fmt.Sprintf("<p id=\"c%dx%d\"></p>", i, j)))
}
w.Write([]byte("</div>\n"))
}
w.Write([]byte("</body>\n"))
flusher.Flush()
// Listen to ch for updates and write to response
for p := range ch {
s := fmt.Sprintf("<style>#%s{background:#000}</style>\n", p)
_, err := w.Write([]byte(s))
if err != nil {
return
}
flusher.Flush()
}
}
@straube
Copy link

straube commented May 6, 2019

I was playing with this snippet and some extra CSS. It seems possible to add a grid covering an entire page.

To do that, it's required to wrap the tracking points in another element (a <div>, perhaps) and make it fixed positioned at 0 x 0. Then add pointer-events: none; to it, pointer-events: auto; to the p, and pointer-events: none; to p:hover.

Even it's not possible (I guess) to get the viewport size on the server side, one could stretch the grid to cover a page using CSS flexbox or grid layout. It's not a perfect solution but it could work.

Here is a summary of the styling I mentioned above:

.track-wrapper {
    position: fixed;
    top: 0; left: 0;
    pointer-events: none;
}

.p {
    pointer-events: auto;
}

.p:hover {
    pointer-events: none;
}

One known issue of this approach is some glitches when hovering a link, for example. The cursor will quickly toggle between pointer and arrow while the mouse is moving.

@fr3fou
Copy link

fr3fou commented Jul 1, 2019

How does the live-preview work? I can see that it's writing to the writer but when does it send the request to the client? Shouldn't the client also make a new request to the server to get the new stylesheet? I'm new to go and it's sorta confusing to me.

@wybiral
Copy link
Author

wybiral commented Jul 2, 2019

@fr3fou it uses chunk transfer encoding. So the server connection stays open and wait for more parts of the web page (almost as though it's a really slow loading web page). The server sends pieces of CSS in real-time (as though it's slowly loading, but really it's real-time).

But, yeah, the chunked transfer encoding is key. The http server is allowed to send blocks of HTML over a TCP connection instead of the typical request/response setup.

@fr3fou
Copy link

fr3fou commented Jul 2, 2019

Yea, I figured it out by rewatching the demo and noticing the page never finished loading ^^

Thanks for the quick reply, though! :>

@AHP873
Copy link

AHP873 commented Jul 25, 2019

Where does the data go or how would i be able to capture it?

@allyraza
Copy link

it pretty simple actually there two windows open as we can see in the demo, one move the cursor other one draws

  1. connections are kept open and response is sent in chunks
  2. one handler send blocks with style hover background image
  3. on hover image is requested from the server ( secret sauce making it tick)
  4. handler for image with coordinates (x,y) puts the data on the go channel
  5. watch handler go throughs the channel and push it to the other window

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment