├── .github └── workflows │ └── static.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── backend ├── .gitignore ├── cursors │ ├── config.cue │ ├── cursors.go │ ├── db.go │ ├── migrations │ │ ├── 1_create_tables.up.sql │ │ ├── 2_path_url.up.sql │ │ └── 3_url_index.up.sql │ ├── pubsub.go │ ├── subscribe.go │ └── validate.go ├── encore.app ├── go.mod └── go.sum ├── demo ├── index.html └── style.css ├── local-demo ├── cursors.min.js ├── index.html └── style.css ├── logo.png └── script ├── .prettierignore ├── .prettierrc ├── dist └── cursors.min.js ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── assets ├── mac.svg ├── tux.svg └── win.svg ├── config.js ├── cursors.js └── ws.js /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: './demo' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .encore 4 | encore.gen 5 | encore.gen.go 6 | encore.gen.cue 7 | 8 | node_modules 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ansar Smagulov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run-backend build-script run-demo lint-script 2 | 3 | run-backend: 4 | @cd backend && encore run 5 | build-script: 6 | @cd script && npm run build && rm ../local-demo/cursors.min.js && cp dist/cursors.min.js ../local-demo/cursors.min.js 7 | run-demo: 8 | @cd local-demo && python3 -m http.server 8000 9 | lint-script: 10 | @cd script && npm run lint -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Encursors logo 4 | 5 |

6 | 7 |

8 | Encursors 9 |

10 | 11 | Ever feel like a lone wanderer on the digital plains of the web? Do your static websites seem just a tad too... *static*? Fear not, for Encursors is here to help! This nifty little tool transforms your website into a bustling village square. With a simple script, Encursors displays each visitor's cursor movements in real time, letting everyone see where everyone else is looking. It's like a party on your page, and everyone's invited! 12 | 13 | The backend is built and hosted with [Encore](https://encore.dev), a development platform for building event-driven and distributed systems. You can run your own instance by cloning the repostory. 14 | 15 | > [!NOTE] 16 | > Encursors does not display cursors or track users on mobile devices. 17 | 18 | ## Demo 19 | You can see Encursors in action on our [demo page](https://anfragment.github.io/encursors/). Open the page in multiple tabs or devices to see the cursors move in real time! 20 | 21 | ## Features 22 | - Displays the flag of the country the visitor is from alongside their cursor. 23 | - Custom cursors based on the visitor's operating system. 24 | - Respects the [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) setting by not displaying the cursors when it is enabled. 25 | - No cookies or tracking of any kind. Cursor data is permanently deleted after a visitor leaves the page. 26 | - Fully open source and free to use. 27 | 28 | ## Installation 29 | To install Encursors, simply add the following script tag to your website's HTML: 30 | ```html 31 | 32 | ``` 33 | 34 | ## Configuration options 35 | You can configure Encursors by passing options to the script tag as data attributes. Here are the available options: 36 | - `data-api-url`: The base URL of the API. Set if you're running your own instance. Should not include the protocol or the trailing slash. 37 | - `data-z-index`: The z-index of the cursor elements. Optional. 38 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | encore.gen.go 2 | encore.gen.cue 3 | /.encore 4 | /encore.gen 5 | -------------------------------------------------------------------------------- /backend/cursors/config.cue: -------------------------------------------------------------------------------- 1 | AllowLocalhost: bool | *false 2 | MinEventTimeoutMs: 1500 3 | 4 | if #Meta.Environment.Type == "development" { 5 | AllowLocalhost: true 6 | } -------------------------------------------------------------------------------- /backend/cursors/cursors.go: -------------------------------------------------------------------------------- 1 | package cursors 2 | 3 | import ( 4 | "context" 5 | 6 | "encore.dev/beta/errs" 7 | "encore.dev/config" 8 | "encore.dev/rlog" 9 | ) 10 | 11 | type Config struct { 12 | AllowLocalhost config.Bool 13 | MinEventTimeoutMs config.Int 14 | } 15 | 16 | var cfg = config.Load[*Config]() 17 | 18 | type CursorOS int 19 | 20 | const ( 21 | CursorOSMacOS CursorOS = iota 22 | CursorOSWindows 23 | CursorOSLinux 24 | ) 25 | 26 | type Cursor struct { 27 | Id string `json:"id"` 28 | Country string `json:"country"` 29 | OS CursorOS `json:"os"` 30 | URL string `json:"url"` 31 | PosX int `json:"posX"` 32 | PosY int `json:"posY"` 33 | } 34 | 35 | type GetCursors struct { 36 | Cursors []*Cursor `json:"cursors"` 37 | } 38 | 39 | type GetCursorsParams struct { 40 | URL string 41 | } 42 | 43 | // Cursors returns all cursors for a given path. 44 | // 45 | //encore:api public method=GET path=/cursors 46 | func Cursors(ctx context.Context, p *GetCursorsParams) (GetCursors, error) { 47 | if !validateURL(p.URL) { 48 | return GetCursors{}, &errs.Error{ 49 | Code: errs.InvalidArgument, 50 | Message: "invalid URL", 51 | } 52 | } 53 | 54 | cursors, err := getCursorsByURLFromDB(ctx, p.URL) 55 | if err != nil { 56 | rlog.Error("failed to retrieve cursors", "error", err) 57 | return GetCursors{}, &errs.Error{ 58 | Code: errs.Internal, 59 | Message: "failed to retrieve cursors", 60 | } 61 | } 62 | return GetCursors{Cursors: cursors}, nil 63 | } 64 | -------------------------------------------------------------------------------- /backend/cursors/db.go: -------------------------------------------------------------------------------- 1 | package cursors 2 | 3 | import ( 4 | "context" 5 | 6 | "encore.dev/storage/sqldb" 7 | ) 8 | 9 | var db = sqldb.NewDatabase("cursors", sqldb.DatabaseConfig{ 10 | Migrations: "./migrations", 11 | }) 12 | 13 | func writeCursorToDB(ctx context.Context, cursor *Cursor) error { 14 | _, err := db.Exec(ctx, "INSERT INTO cursors (id, country, os, url, pos_x, pos_y) VALUES ($1, $2, $3, $4, $5, $6)", 15 | cursor.Id, cursor.Country, cursor.OS, cursor.URL, cursor.PosX, cursor.PosY) 16 | return err 17 | } 18 | 19 | func getCursorsByURLFromDB(ctx context.Context, url string) ([]*Cursor, error) { 20 | rows, err := db.Query(ctx, "SELECT id, country, os, url, pos_x, pos_y FROM cursors WHERE url = $1", url) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer rows.Close() 25 | 26 | var cursors []*Cursor 27 | for rows.Next() { 28 | var cursor Cursor 29 | if err := rows.Scan(&cursor.Id, &cursor.Country, &cursor.OS, &cursor.URL, &cursor.PosX, &cursor.PosY); err != nil { 30 | return nil, err 31 | } 32 | cursors = append(cursors, &cursor) 33 | } 34 | return cursors, nil 35 | } 36 | 37 | func updateCursorInDB(ctx context.Context, cursor *Cursor) error { 38 | _, err := db.Exec(ctx, "UPDATE cursors SET pos_x = $1, pos_y = $2 WHERE id = $3", cursor.PosX, cursor.PosY, cursor.Id) 39 | return err 40 | } 41 | 42 | func deleteCursorFromDB(ctx context.Context, id string) error { 43 | _, err := db.Exec(ctx, "DELETE FROM cursors WHERE id = $1", id) 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /backend/cursors/migrations/1_create_tables.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE cursors ( 2 | id CHAR(36) PRIMARY KEY, 3 | os INT NOT NULL, 4 | country CHAR(2) NOT NULL, 5 | path VARCHAR(2048) NOT NULL, 6 | pos_x INT NOT NULL, 7 | pos_y INT NOT NULL 8 | ); -------------------------------------------------------------------------------- /backend/cursors/migrations/2_path_url.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE cursors 2 | RENAME COLUMN path TO url; -------------------------------------------------------------------------------- /backend/cursors/migrations/3_url_index.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX cursors_url_index ON cursors (url); -------------------------------------------------------------------------------- /backend/cursors/pubsub.go: -------------------------------------------------------------------------------- 1 | package cursors 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "encore.dev/pubsub" 8 | ) 9 | 10 | type CursorEventType string 11 | 12 | const ( 13 | CursorEventTypeEnter CursorEventType = "enter" 14 | CursorEventTypeMove CursorEventType = "move" 15 | CursorEventTypeLeave CursorEventType = "leave" 16 | ) 17 | 18 | type CursorEvent struct { 19 | Type CursorEventType `json:"type"` 20 | Cursor *Cursor `json:"cursor"` 21 | } 22 | 23 | var subscribersMu sync.RWMutex 24 | 25 | // subscribers maps URLs to a map of subscriber IDs to channels. 26 | var subscribers = make(map[string]map[string]chan *CursorEvent) 27 | 28 | // fanout sends the cursor to all subscribers. 29 | func fanout(ctx context.Context, event *CursorEvent) error { 30 | subscribersMu.RLock() 31 | defer subscribersMu.RUnlock() 32 | for _, ch := range subscribers[event.Cursor.URL] { 33 | ch <- event 34 | } 35 | return nil 36 | } 37 | 38 | // subToUpdates subscribes a client to cursor updates. 39 | func subToUpdates(id string, url string, ch chan *CursorEvent, done <-chan struct{}) { 40 | subscribersMu.Lock() 41 | defer subscribersMu.Unlock() 42 | if _, ok := subscribers[url]; !ok { 43 | subscribers[url] = make(map[string]chan *CursorEvent) 44 | } 45 | subscribers[url][id] = ch 46 | go func() { 47 | <-done 48 | subscribersMu.Lock() 49 | defer subscribersMu.Unlock() 50 | delete(subscribers[url], id) 51 | }() 52 | } 53 | 54 | var CursorEvents = pubsub.NewTopic[*CursorEvent]("cursor-events", pubsub.TopicConfig{ 55 | DeliveryGuarantee: pubsub.AtLeastOnce, 56 | }) 57 | 58 | var _ = pubsub.NewSubscription[*CursorEvent](CursorEvents, "cursor-events-fanout", pubsub.SubscriptionConfig[*CursorEvent]{ 59 | Handler: fanout, 60 | }) 61 | -------------------------------------------------------------------------------- /backend/cursors/subscribe.go: -------------------------------------------------------------------------------- 1 | package cursors 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "strconv" 8 | "time" 9 | 10 | "encore.dev/metrics" 11 | "encore.dev/rlog" 12 | "github.com/google/uuid" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | type CursorEnterWebsocketPayload struct { 17 | Id string `json:"id"` 18 | Country string `json:"country"` 19 | OS CursorOS `json:"os"` 20 | PosX int `json:"posX"` 21 | PosY int `json:"posY"` 22 | } 23 | 24 | type CursorMoveWebsocketPayload struct { 25 | Id string `json:"id"` 26 | PosX int `json:"posX"` 27 | PosY int `json:"posY"` 28 | } 29 | 30 | type CursorLeaveWebsocketPayload struct { 31 | Id string `json:"id"` 32 | } 33 | 34 | var upgrader = websocket.Upgrader{ 35 | CheckOrigin: func(r *http.Request) bool { 36 | return true 37 | }, 38 | } 39 | 40 | var TotalClients = metrics.NewCounter[uint64]("total_clients", metrics.CounterConfig{}) 41 | 42 | // Subscribe subscribes to cursor updates for a given URL. 43 | // 44 | //encore:api public raw method=GET path=/subscribe 45 | func Subscribe(w http.ResponseWriter, req *http.Request) { 46 | ctx := req.Context() 47 | query := req.URL.Query() 48 | 49 | url := query.Get("url") 50 | if !validateURL(url) { 51 | http.Error(w, "specify valid url in url parameters", http.StatusBadRequest) 52 | return 53 | } 54 | 55 | var os CursorOS 56 | switch query.Get("os") { 57 | case "mac": 58 | os = CursorOSMacOS 59 | case "win": 60 | os = CursorOSWindows 61 | case "linux": 62 | os = CursorOSLinux 63 | default: 64 | http.Error(w, "specify valid os in url parameters", http.StatusBadRequest) 65 | return 66 | } 67 | 68 | country := query.Get("country") 69 | if country == "" || len(country) != 2 { 70 | http.Error(w, "specify valid country in url parameters", http.StatusBadRequest) 71 | return 72 | } 73 | 74 | var posX, posY int 75 | var err error 76 | if query.Get("posX") != "" { 77 | posX, err = strconv.Atoi(query.Get("posX")) 78 | if err != nil { 79 | http.Error(w, "specify valid posX in url parameters", http.StatusBadRequest) 80 | return 81 | } 82 | } 83 | if query.Get("posY") != "" { 84 | posY, err = strconv.Atoi(query.Get("posY")) 85 | if err != nil { 86 | http.Error(w, "specify valid posY in url parameters", http.StatusBadRequest) 87 | return 88 | } 89 | } 90 | 91 | c, err := upgrader.Upgrade(w, req, nil) 92 | if err != nil { 93 | rlog.Error("error upgrading websocket connection", "err", err) 94 | return 95 | } 96 | 97 | TotalClients.Increment() 98 | 99 | id := uuid.New().String() 100 | rlog := rlog.With("cursor_id", id) 101 | 102 | events := make(chan *CursorEvent) 103 | done := make(chan struct{}) 104 | go handleIncomingPubSubEvents(ctx, rlog, id, url, events, done, c) 105 | defer handleClosure(ctx, rlog, events, done, c) 106 | 107 | cursor := &Cursor{ 108 | Id: id, 109 | Country: country, 110 | OS: os, 111 | URL: url, 112 | PosX: posX, 113 | PosY: posY, 114 | } 115 | if err := writeCursorToDB(ctx, cursor); err != nil { 116 | rlog.Error("error writing cursor to db", "err", err) 117 | return 118 | } 119 | defer deleteCursorFromDB(ctx, id) 120 | 121 | handleWSComms(ctx, rlog, cursor, c) 122 | } 123 | 124 | func handleWSComms(ctx context.Context, rlog rlog.Ctx, cursor *Cursor, c *websocket.Conn) { 125 | event := &CursorEvent{ 126 | Type: CursorEventTypeEnter, 127 | Cursor: cursor, 128 | } 129 | if msgId, err := CursorEvents.Publish(ctx, event); err != nil { 130 | rlog.Error("error publishing cursor event", "err", err) 131 | } else { 132 | rlog.Debug("published cursor enter event", "msg_id", msgId) 133 | } 134 | 135 | var lastMessageAt time.Time 136 | 137 | for { 138 | if ctx.Err() != nil { 139 | break 140 | } 141 | 142 | mt, message, err := c.ReadMessage() 143 | if err != nil { 144 | if !websocket.IsCloseError(err, websocket.CloseNormalClosure) { 145 | rlog.Error("error reading message", "err", err) 146 | } 147 | break 148 | } 149 | if mt != websocket.TextMessage { 150 | rlog.Error("unexpected message type", "type", mt) 151 | break 152 | } 153 | 154 | if time.Since(lastMessageAt) < time.Duration(cfg.MinEventTimeoutMs())*time.Millisecond { 155 | rlog.Error("received message too quickly, ignoring", "elapsed", time.Since(lastMessageAt)) 156 | lastMessageAt = time.Now() 157 | continue 158 | } 159 | lastMessageAt = time.Now() 160 | 161 | pos := [2]int{} 162 | if err := json.Unmarshal(message, &pos); err != nil { 163 | rlog.Error("error unmarshalling message", "err", err) 164 | break 165 | } 166 | 167 | rlog.Debug("received cursor position", "pos", pos) 168 | 169 | cursor.PosX = pos[0] 170 | cursor.PosY = pos[1] 171 | 172 | if err := updateCursorInDB(ctx, cursor); err != nil { 173 | rlog.Error("error updating cursor in db", "err", err) 174 | break 175 | } 176 | 177 | event = &CursorEvent{ 178 | Type: CursorEventTypeMove, 179 | Cursor: cursor, 180 | } 181 | if msgId, err := CursorEvents.Publish(ctx, event); err != nil { 182 | rlog.Error("error publishing cursor event", "err", err) 183 | } else { 184 | rlog.Debug("published cursor move event", "msg_id", msgId) 185 | } 186 | } 187 | 188 | event.Type = CursorEventTypeLeave 189 | if msgId, err := CursorEvents.Publish(ctx, event); err != nil { 190 | rlog.Error("error publishing cursor event", "err", err) 191 | } else { 192 | rlog.Debug("published cursor leave event", "msg_id", msgId) 193 | } 194 | } 195 | 196 | func handleIncomingPubSubEvents(_ context.Context, rlog rlog.Ctx, id string, url string, eventsCh chan *CursorEvent, doneCh <-chan struct{}, c *websocket.Conn) { 197 | subToUpdates(id, url, eventsCh, doneCh) 198 | for { 199 | select { 200 | case event := <-eventsCh: 201 | if event.Cursor.Id == id { 202 | continue 203 | } 204 | msg := struct { 205 | Type CursorEventType `json:"type"` 206 | Payload interface{} `json:"payload"` 207 | }{ 208 | Type: event.Type, 209 | } 210 | switch event.Type { 211 | case CursorEventTypeEnter: 212 | msg.Payload = CursorEnterWebsocketPayload{ 213 | Id: event.Cursor.Id, 214 | Country: event.Cursor.Country, 215 | OS: event.Cursor.OS, 216 | PosX: event.Cursor.PosX, 217 | PosY: event.Cursor.PosY, 218 | } 219 | case CursorEventTypeMove: 220 | msg.Payload = CursorMoveWebsocketPayload{ 221 | Id: event.Cursor.Id, 222 | PosX: event.Cursor.PosX, 223 | PosY: event.Cursor.PosY, 224 | } 225 | case CursorEventTypeLeave: 226 | msg.Payload = CursorLeaveWebsocketPayload{ 227 | Id: event.Cursor.Id, 228 | } 229 | default: 230 | rlog.Error("unknown cursor event type", "type", event.Type) 231 | continue 232 | } 233 | 234 | if err := c.WriteJSON(msg); err != nil { 235 | rlog.Error("error writing JSON", "err", err) 236 | return 237 | } 238 | case <-doneCh: 239 | return 240 | } 241 | } 242 | } 243 | 244 | func handleClosure(ctx context.Context, rlog rlog.Ctx, events chan *CursorEvent, done chan struct{}, c *websocket.Conn) { 245 | rlog.Debug("closing websocket connection") 246 | close(done) 247 | close(events) 248 | c.Close() 249 | } 250 | -------------------------------------------------------------------------------- /backend/cursors/validate.go: -------------------------------------------------------------------------------- 1 | package cursors 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "strings" 7 | 8 | "encore.dev/rlog" 9 | ) 10 | 11 | // validateURL validates the URL to ensure it can be used for fetching and subscribing to cursor updates. 12 | // 13 | // Returns true if the url is valid, false otherwise. 14 | func validateURL(urlToValidate string) bool { 15 | url, err := url.Parse(urlToValidate) 16 | if err != nil { 17 | rlog.Debug("failed to parse url", "err", err) 18 | return false 19 | } 20 | 21 | if cfg.AllowLocalhost() { 22 | if strings.HasSuffix(url.Host, "localhost") { 23 | return true 24 | } 25 | if ip := net.ParseIP(url.Host); ip != nil && ip.IsLoopback() { 26 | return true 27 | } 28 | } 29 | 30 | if url.Host == "" { 31 | return false 32 | } 33 | if url.Fragment != "" { 34 | return false 35 | } 36 | if strings.HasSuffix(url.Host, "localhost") { 37 | return false 38 | } 39 | if ip := net.ParseIP(url.Host); ip != nil && (ip.IsLoopback() || ip.IsPrivate()) { 40 | return false 41 | } 42 | 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /backend/encore.app: -------------------------------------------------------------------------------- 1 | { 2 | "id": "encursors-ypdi", 3 | "global_cors": { 4 | "allow_origins_without_credentials": ["*"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module encore.app 2 | 3 | go 1.18 4 | 5 | require ( 6 | encore.dev v1.34.3 7 | github.com/google/uuid v1.6.0 8 | github.com/gorilla/websocket v1.5.1 9 | ) 10 | 11 | require ( 12 | github.com/jackc/pgpassfile v1.0.0 // indirect 13 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 14 | github.com/jackc/pgx/v5 v5.5.4 // indirect 15 | github.com/jackc/puddle/v2 v2.2.1 // indirect 16 | golang.org/x/crypto v0.21.0 // indirect 17 | golang.org/x/net v0.23.0 // indirect 18 | golang.org/x/sync v0.1.0 // indirect 19 | golang.org/x/text v0.14.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | encore.dev v1.34.3 h1:jO+8jBqq5/MShiRHSRGPBuTq003YR6KnnspgD+fkMBg= 2 | encore.dev v1.34.3/go.mod h1:XdWK6bKKAVzutmOKpC5qzalDQJLNfRCF/YCgA7OUZ3E= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 6 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 8 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 9 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 10 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 11 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 12 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 13 | github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= 14 | github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 15 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 16 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 23 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 24 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 25 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 26 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 27 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 28 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 30 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Encursors Demo 7 | 8 | 9 | 10 |
11 |

Encursors Demo

12 | 13 |

14 | Stopping by Woods on a Snowy Evening, by Robert Frost 15 |

16 |

17 | Whose woods these are I think I know. 18 | His house is in the village though; 19 | He will not see me stopping here 20 | To watch his woods fill up with snow. 21 | 22 | My little horse must think it queer 23 | To stop without a farmhouse near 24 | Between the woods and frozen lake 25 | The darkest evening of the year. 26 | 27 | He gives his harness bells a shake 28 | To ask if there is some mistake. 29 | The only other sound’s the sweep 30 | Of easy wind and downy flake. 31 | 32 | The woods are lovely, dark and deep, 33 | But I have promises to keep, 34 | And miles to go before I sleep, 35 | And miles to go before I sleep. 36 |

37 | 38 |

39 | Lines Written in Early Spring, by William Wordsworth 40 |

41 |

42 | I heard a thousand blended notes, 43 | While in a grove I sate reclined, 44 | In that sweet mood when pleasant thoughts 45 | Bring sad thoughts to the mind. 46 | 47 | To her fair works did Nature link 48 | The human soul that through me ran; 49 | And much it grieved my heart to think 50 | What man has made of man. 51 | 52 | Through primrose tufts, in that green bower, 53 | The periwinkle trailed its wreaths; 54 | And ’tis my faith that every flower 55 | Enjoys the air it breathes. 56 | 57 | The birds around me hopped and played, 58 | Their thoughts I cannot measure:— 59 | But the least motion which they made 60 | It seemed a thrill of pleasure. 61 | 62 | The budding twigs spread out their fan, 63 | To catch the breezy air; 64 | And I must think, do all I can, 65 | That there was pleasure there. 66 | 67 | If this belief from heaven be sent, 68 | If such be Nature’s holy plan, 69 | Have I not reason to lament 70 | What man has made of man? 71 |

72 | 73 |

74 | The Summer Day, by Mary Oliver 75 |

76 |

77 | Who made the world? 78 | Who made the swan, and the black bear? 79 | Who made the grasshopper? 80 | This grasshopper, I mean— 81 | the one who has flung herself out of the grass, 82 | the one who is eating sugar out of my hand, 83 | who is moving her jaws back and forth instead of up and down— 84 | who is gazing around with her enormous and complicated eyes. 85 | Now she lifts her pale forearms and thoroughly washes her face. 86 | Now she snaps her wings open, and floats away. 87 | I don't know exactly what a prayer is. 88 | I do know how to pay attention, how to fall down 89 | into the grass, how to kneel down in the grass, 90 | how to be idle and blessed, how to stroll through the fields, 91 | which is what I have been doing all day. 92 | Tell me, what else should I have done? 93 | Doesn't everything die at last, and too soon? 94 | Tell me, what is it you plan to do 95 | with your one wild and precious life? 96 |

97 | 98 |

99 | To Autumn, by John Keats 100 |

101 |

102 | Season of mists and mellow fruitfulness, 103 | Close bosom-friend of the maturing sun; 104 | Conspiring with him how to load and bless 105 | With fruit the vines that round the thatch-eves run; 106 | To bend with apples the moss’d cottage-trees, 107 | And fill all fruit with ripeness to the core; 108 | To swell the gourd, and plump the hazel shells 109 | With a sweet kernel; to set budding more, 110 | And still more, later flowers for the bees, 111 | Until they think warm days will never cease, 112 | For summer has o’er-brimm’d their clammy cells. 113 | 114 | Who hath not seen thee oft amid thy store? 115 | Sometimes whoever seeks abroad may find 116 | Thee sitting careless on a granary floor, 117 | Thy hair soft-lifted by the winnowing wind; 118 | Or on a half-reap’d furrow sound asleep, 119 | Drowsed with the fume of poppies, while thy hook 120 | Spares the next swath and all its twined flowers: 121 | And sometimes like a gleaner thou dost keep 122 | Steady thy laden head across a brook; 123 | Or by a cider-press, with patient look, 124 | Thou watchest the last oozings, hours by hours. 125 | 126 | Where are the songs of Spring? Ay, where are they? 127 | Think not of them, thou hast thy music too,— 128 | While barred clouds bloom the soft-dying day, 129 | And touch the stubble-plains with rosy hue; 130 | Then in a wailful choir the small gnats mourn 131 | Among the river sallows, borne aloft 132 | Or sinking as the light wind lives or dies; 133 | And full-grown lambs loud bleat from hilly bourn; 134 | Hedge-crickets sing; and now with treble soft 135 | The redbreast whistles from a garden-croft, 136 | And gathering swallows twitter in the skies. 137 |

138 |
139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #f0f0f0; 3 | font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif; 4 | font-weight: normal; 5 | } 6 | 7 | main { 8 | max-width: 600px; 9 | margin: 0 auto; 10 | } 11 | 12 | p { 13 | white-space: pre-line; 14 | margin-top: 0; 15 | margin-bottom: 3em; 16 | } -------------------------------------------------------------------------------- /local-demo/cursors.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var a="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='10 7.4 11.4 18.1'%3e%3cg fill='none' fill-rule='evenodd' transform='translate(10 7)'%3e%3cpath d='m6.148 18.473 1.863-1.003 1.615-.839-2.568-4.816h4.332l-11.379-11.408v16.015l3.316-3.221z' fill='white'/%3e%3cpath d='m6.431 17 1.765-.941-2.775-5.202h3.604l-8.025-8.043v11.188l2.53-2.442z' fill='black'/%3e%3c/g%3e%3c/svg%3e";function r(a,r,i,c){let e=new WebSocket(a),n=!1,t=0,u=0;const o=function(a,r){let i,c,e=0;return function(...n){if(c=n,i)return;const t=Date.now();if(t-e>r)return e=t,void a(...c);i=setTimeout((()=>{e=Date.now(),a(...c),i=null}),r-(t-e))}}((a=>{if("number"==typeof a.clientX&&(t=a.clientX,u=a.clientY),!n)return;const r=Math.floor(t+window.scrollX),i=Math.floor(u+window.scrollY);e.send(JSON.stringify([r,i]))}),2e3);document.addEventListener("mousemove",o),document.addEventListener("mouseenter",o),document.addEventListener("scroll",o),e.onopen=function(){n=!0},e.onclose=function(){n=!1,setTimeout((()=>{e=new WebSocket(a)}),5e3)},e.onmessage=function(a){const e=JSON.parse(a.data);switch(e.type){case"enter":r(e.payload);break;case"move":i(e.payload);break;case"leave":c(e.payload)}},e.onerror=function(a){console.error("WebSocket error:",a)}}function i(a,r){var i=Object.keys(a);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(a);r&&(c=c.filter((function(r){return Object.getOwnPropertyDescriptor(a,r).enumerable}))),i.push.apply(i,c)}return i}function c(a){for(var r=1;r=0||(e[i]=a[i]);return e}(a,r);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(a);for(c=0;c=0||Object.prototype.propertyIsEnumerable.call(a,i)&&(e[i]=a[i])}return e}function t(a,r){return function(a){if(Array.isArray(a))return a}(a)||function(a,r){var i=null==a?null:"undefined"!=typeof Symbol&&a[Symbol.iterator]||a["@@iterator"];if(null!=i){var c,e,n,t,u=[],o=!0,A=!1;try{if(n=(i=i.call(a)).next,0===r);else for(;!(o=(c=n.call(i)).done)&&(u.push(c.value),u.length!==r);o=!0);}catch(a){A=!0,e=a}finally{try{if(!o&&null!=i.return&&(t=i.return(),Object(t)!==t))return}finally{if(A)throw e}}return u}}(a,r)||function(a,r){if(!a)return;if("string"==typeof a)return u(a,r);var i=Object.prototype.toString.call(a).slice(8,-1);"Object"===i&&a.constructor&&(i=a.constructor.name);if("Map"===i||"Set"===i)return Array.from(a);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return u(a,r)}(a,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(a,r){(null==r||r>a.length)&&(r=a.length);for(var i=0,c=new Array(r);i1&&void 0!==arguments[1]?arguments[1]:{};return p[a]||function(a){if(!a)return;p[a.id]=a}(s(A,a)),function(a,r){if(!a)return null;var i=r||{},e=i.deprecated;a.allTimezones;var t=n(a,m),u=e?a.allTimezones:a.timezones;return c(c({},t),{},{timezones:u})}(p[a],r)}function v(a){return f[a]||function(a){if(!a)return;f[a.name]=a,M=Object.keys(a).length}(function(a,r){var i=a.timezones[r];if(!i)return null;var e=i.a,n=void 0===e?null:e,t=c(c({},n?a.timezones[n]:{}),a.timezones[r]),u=t.c||[],o=t.u,A=Number.isInteger(t.d)?t.d:o,s={name:r,countries:u,utcOffset:o,utcOffsetStr:h(o),dstOffset:A,dstOffsetStr:h(A),aliasOf:n};return i.r&&(s.deprecated=!0),s}(A,a)),f[a]?c({},f[a]):null}function S(a){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return((v(a)||{}).countries||[]).map((function(a){return g(a,r)}))}var E={getCountry:g,getTimezone:v,getAllCountries:function(){var a=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return Object.keys(A.countries).reduce((function(r,i){return Object.assign(r,e({},i,g(i,a)))}),{})},getAllTimezones:function(){var a=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return l!==M&&Object.keys(A.timezones).forEach(v),function(a,r){var i=r||{};return!0===i.deprecated?a:Object.keys(a).reduce((function(r,i){return a[i].deprecated||Object.assign(r,e({},i,a[i])),r}),{})}(f,a)},getTimezonesForCountry:function(a){var r=g(a,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{});return r?(r.timezones||[]).map(v):null},getCountriesForTimezone:S,getCountryForTimezone:function(a){return t(S(a,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}),1)[0]||null}};function C(a,r){return Math.min(Math.max(0,a),document.documentElement.scrollHeight-1.5*r-1)}function P(a,r){return Math.min(Math.max(0,a),document.documentElement.scrollWidth-1.5*r-1)}(async()=>{console.log("Collaborative cursors experience on this page is powered by Encursors. Learn more at:\nhttps://github.com/anfragment/encursors");if(!0===window.matchMedia("(prefers-reduced-motion: reduce)")||!0===window.matchMedia("(prefers-reduced-motion: reduce)").matches)return void console.debug("Reduced motion is enabled, not showing cursors");const i=function(){const a=window.navigator?.userAgentData?.platform||window.navigator.platform,r=["Win32","Win64","Windows","WinCE"];if(-1!==["macOS","Macintosh","MacIntel","MacPPC","Mac68K"].indexOf(a))return"mac";if(-1!==r.indexOf(a))return"win";if(/Linux/.test(a))return"linux";return null}();if(!i)return void console.debug("Unsupported OS, not showing cursors");const c=E.getCountryForTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);if(!c)return void console.debug("Could not determine country, not showing cursors");const e=c.id,n=document.currentScript,t=n.getAttribute("data-api-url")||"prod-encursors-ypdi.encr.app",u=n.getAttribute("data-z-index"),o=window.location.href.split("?")[0].split("#")[0],A=await fetch(`${o.startsWith("https")?"https":"http"}://${t}/cursors?url=${o}`),s=await A.json();for(const a of s.cursors||[])h(a);function h(r){const i=document.createElement("div");i.setAttribute("data-cursor-id",r.id),i.style.position="absolute",u&&(i.style.zIndex=u),i.style.transition="left 0.2s, top 0.2s",0===r.posX&&0===r.posY?i.style.display="none":i.style.display="flex",i.style.alignItems="center",i.style.justifyContent="center",i.style.pointerEvents="none";const c=document.createElement("img");switch(r.os){case 0:default:c.src=a;break;case 1:c.src="data:image/svg+xml,%3csvg viewBox='0 0 492 779' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m41 41h41v41h-41z'/%3e%3cpath d='m82 82h41v41h-41z'/%3e%3cpath d='m123 123h41v41h-41z'/%3e%3cpath d='m164 164h41v41h-41z'/%3e%3cpath d='m205 205h41v41h-41z'/%3e%3cpath d='m246 246h41v41h-41z'/%3e%3cpath d='m287 287h41v41h-41z'/%3e%3cpath d='m328 328h41v41h-41z'/%3e%3cpath d='m369 369h41v41h-41z'/%3e%3cpath d='m410 410h41v41h-41z'/%3e%3cpath d='m451.01 451h41v41h-41z'/%3e%3cpath d='m451.01 492h41v41h-41z'/%3e%3cpath d='m410 492h41v41h-41z'/%3e%3cpath d='m369 492h41v41h-41z'/%3e%3cpath d='m328 492h41v41h-41z'/%3e%3cpath d='m287 492h41v41h-41z'/%3e%3cpath d='m287 533h41v41h-41z'/%3e%3cpath d='m328 574h41v41h-41z'/%3e%3cpath d='m328 615h41v41h-41z'/%3e%3cpath d='m369 656.03h41v41h-41z'/%3e%3cpath d='m369 697.03h41v41h-41z'/%3e%3cpath d='m328 738.03h41v41h-41z'/%3e%3cpath d='m287 738.03h41v41h-41z'/%3e%3cpath d='m246 697.03h41v41h-41z'/%3e%3cpath d='m246 656.03h41v41h-41z'/%3e%3cpath d='m205 615h41v41h-41z'/%3e%3cpath d='m205 574h41v41h-41z'/%3e%3cpath d='m164 533h41v41h-41z'/%3e%3cpath d='m123 574h41v41h-41z'/%3e%3cpath d='m82 615h41v41h-41z'/%3e%3cpath d='m41 656.03h41v41h-41z'/%3e%3cpath d='m0 656.03h41v41h-41z'/%3e%3cpath d='m0 615h41v41h-41z'/%3e%3cpath d='m0 574h41v41h-41z'/%3e%3cpath d='m0 533h41v41h-41z'/%3e%3cpath d='m0 492h41v41h-41z'/%3e%3cpath d='m0 451h41v41h-41z'/%3e%3cpath d='m0 410h41v41h-41z'/%3e%3cpath d='m0 369h41v41h-41z'/%3e%3cpath d='m0 328h41v41h-41z'/%3e%3cpath d='m0 287h41v41h-41z'/%3e%3cpath d='m0 246h41v41h-41z'/%3e%3cpath d='m0 205h41v41h-41z'/%3e%3cpath d='m0 164h41v41h-41z'/%3e%3cpath d='m0 123h41v41h-41z'/%3e%3cpath d='m0 82h41v41h-41z'/%3e%3cpath d='m0 41h41v41h-41z'/%3e%3cpath d='m0 0h41v41h-41z'/%3e%3cg fill='white'%3e%3cpath d='m41 82h41v41h-41z'/%3e%3cpath d='m82 123h41v41h-41z'/%3e%3cpath d='m41 123h41v41h-41z'/%3e%3cpath d='m41 164h41v41h-41z'/%3e%3cpath d='m82 164h41v41h-41z'/%3e%3cpath d='m123 164h41v41h-41z'/%3e%3cpath d='m41 205h41v41h-41z'/%3e%3cpath d='m82 205h41v41h-41z'/%3e%3cpath d='m123 205h41v41h-41z'/%3e%3cpath d='m164 205h41v41h-41z'/%3e%3cpath d='m41 615h41v41h-41z'/%3e%3cpath d='m41 574h41v41h-41z'/%3e%3cpath d='m82 574h41v41h-41z'/%3e%3cpath d='m246 574h41v41h-41z'/%3e%3cpath d='m287 615h41v41h-41z'/%3e%3cpath d='m246 615h41v41h-41z'/%3e%3cpath d='m287 574h41v41h-41z'/%3e%3cpath d='m287 656.03h41v41h-41z'/%3e%3cpath d='m328 656.03h41v41h-41z'/%3e%3cpath d='m287 697.03h41v41h-41z'/%3e%3cpath d='m328 697.03h41v41h-41z'/%3e%3cpath d='m205 533h41v41h-41z'/%3e%3cpath d='m246 533h41v41h-41z'/%3e%3cpath d='m123 533h41v41h-41z'/%3e%3cpath d='m82 533h41v41h-41z'/%3e%3cpath d='m41 533h41v41h-41z'/%3e%3cpath d='m41 492h41v41h-41z'/%3e%3cpath d='m82 492h41v41h-41z'/%3e%3cpath d='m123 492h41v41h-41z'/%3e%3cpath d='m164 492h41v41h-41z'/%3e%3cpath d='m205 492h41v41h-41z'/%3e%3cpath d='m246 492h41v41h-41z'/%3e%3cpath d='m410 451h41v41h-41z'/%3e%3cpath d='m369 451h41v41h-41z'/%3e%3cpath d='m328 451h41v41h-41z'/%3e%3cpath d='m287 451h41v41h-41z'/%3e%3cpath d='m246 451h41v41h-41z'/%3e%3cpath d='m205 451h41v41h-41z'/%3e%3cpath d='m164 451h41v41h-41z'/%3e%3cpath d='m123 451h41v41h-41z'/%3e%3cpath d='m82 451h41v41h-41z'/%3e%3cpath d='m41 451h41v41h-41z'/%3e%3cpath d='m41 410h41v41h-41z'/%3e%3cpath d='m82 410h41v41h-41z'/%3e%3cpath d='m41 369h41v41h-41z'/%3e%3cpath d='m41 328h41v41h-41z'/%3e%3cpath d='m82 369h41v41h-41z'/%3e%3cpath d='m123 410h41v41h-41z'/%3e%3cpath d='m164 410h41v41h-41z'/%3e%3cpath d='m123 369h41v41h-41z'/%3e%3cpath d='m82 328h41v41h-41z'/%3e%3cpath d='m41 287h41v41h-41z'/%3e%3cpath d='m41 246h41v41h-41z'/%3e%3cpath d='m82 287h41v41h-41z'/%3e%3cpath d='m123 328h41v41h-41z'/%3e%3cpath d='m164 369h41v41h-41z'/%3e%3cpath d='m205 410h41v41h-41z'/%3e%3cpath d='m82 246h41v41h-41z'/%3e%3cpath d='m123 287h41v41h-41z'/%3e%3cpath d='m164 328h41v41h-41z'/%3e%3cpath d='m205 369h41v41h-41z'/%3e%3cpath d='m246 410h41v41h-41z'/%3e%3cpath d='m287 410h41v41h-41z'/%3e%3cpath d='m328 410h41v41h-41z'/%3e%3cpath d='m369 410h41v41h-41z'/%3e%3cpath d='m328 369h41v41h-41z'/%3e%3cpath d='m287 328h41v41h-41z'/%3e%3cpath d='m246 287h41v41h-41z'/%3e%3cpath d='m205 246h41v41h-41z'/%3e%3cpath d='m164 246h41v41h-41z'/%3e%3cpath d='m205 287h41v41h-41z'/%3e%3cpath d='m246 328h41v41h-41z'/%3e%3cpath d='m287 369h41v41h-41z'/%3e%3cpath d='m246 369h41v41h-41z'/%3e%3cpath d='m205 328h41v41h-41z'/%3e%3cpath d='m164 287h41v41h-41z'/%3e%3cpath d='m123 246h41v41h-41z'/%3e%3c/g%3e%3c/svg%3e";break;case 2:c.src="data:image/svg+xml,%3csvg enable-background='new 0 0 712 860' height='860' viewBox='0 0 712 860' width='712' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m683.91 677.74c-.01-.01-.021-.021-.021-.03-6.069-6.85-8.96-19.55-12.069-33.08-3.101-13.52-6.57-28.1-17.67-37.55-.021-.021-.051-.04-.07-.061-2.2-1.92-4.46-3.539-6.73-4.89-2.279-1.35-4.59-2.45-6.899-3.32 15.43-45.76 9.38-91.329-6.2-132.5-19.11-50.54-52.48-94.569-77.97-124.689-28.53-35.99-56.431-70.15-55.88-120.61.849-77.01 8.469-219.82-127.051-220.01-5.51-.01-11.26.22-17.25.7-151.44 12.19-111.27 172.19-113.52 225.76-2.77 39.18-10.71 70.06-37.66 108.36-31.65 37.64-76.23 98.57-97.34 162-9.96 29.93-14.7 60.439-10.33 89.319-1.37 1.23-2.67 2.521-3.92 3.841-9.29 9.93-16.16 21.949-23.81 30.039-7.15 7.141-17.33 9.851-28.53 13.86-11.2 4.021-23.49 9.94-30.95 24.26 0 0 0 .011-.01.011-.01.02-.02.05-.04.069-3.51 6.55-4.65 13.62-4.65 20.79 0 6.63.98 13.351 1.97 19.82 2.06 13.46 4.15 26.189 1.38 34.81-8.86 24.23-10 40.98-3.76 53.141 6.26 12.18 19.11 17.55 33.64 20.59 29.06 6.06 68.41 4.56 99.42 21l2.67-5.03-2.64 5.04c33.2 17.36 66.86 23.521 93.71 17.39 19.48-4.439 35.28-16.039 43.4-33.88 21-.1 44.05-9 80.97-11.029 25.05-2.021 56.34 8.899 92.33 6.899.94 3.9 2.301 7.66 4.16 11.23.021.029.04.069.061.1 13.949 27.9 39.869 40.66 67.5 38.48 27.659-2.181 57.069-18.49 80.85-46.78l-4.36-3.66 4.391 3.62c22.66-27.48 60.27-38.87 85.22-53.91 12.47-7.52 22.58-16.939 23.37-30.62.778-13.67-7.252-28.99-25.712-49.48z' fill='%23202020'/%3e%3cg fill='white'%3e%3cpath d='m698.24 726.57c-.48 8.439-6.59 14.71-17.88 21.51-22.561 13.61-62.551 25.45-88.08 56.38-22.181 26.39-49.221 40.88-73.03 42.76s-44.35-8-56.47-32.31l-.021-.05-.029-.061c-7.521-14.3-4.391-36.85 1.939-60.649 6.33-23.79 15.43-48.23 16.65-68.08v-.061c1.279-25.439 2.71-47.66 6.979-64.81 4.28-17.15 11.01-28.75 22.931-35.28.56-.3 1.109-.59 1.659-.86 1.351 22.03 12.261 44.511 31.54 49.37 21.101 5.561 51.511-12.54 64.351-27.31 2.569-.101 5.069-.23 7.5-.29 11.27-.271 20.71.38 30.37 8.82l.029.029.03.021c7.42 6.29 10.95 18.17 14.01 31.47 3.061 13.311 5.5 27.8 14.681 38.13l.01.01.01.021c17.641 19.57 23.311 32.79 22.821 41.24z'/%3e%3cpath d='m269.49 788.95-.01.06v.07c-2.04 26.74-17.12 41.3-40.28 46.59-23.14 5.29-54.53.021-85.87-16.37-.01 0-.02-.01-.03-.01-34.68-18.37-75.93-16.54-102.4-22.07-13.23-2.76-21.87-6.92-25.83-14.64-3.96-7.73-4.05-21.2 4.37-44.17l.04-.101.03-.1c4.17-12.85 1.08-26.91-.94-40.11-2.02-13.189-3.01-25.199 1.5-33.56l.04-.08c5.77-11.12 14.23-15.1 24.73-18.86 10.51-3.77 22.96-6.729 32.79-16.59l.06-.05.05-.05c9.09-9.59 15.92-21.62 23.91-30.15 6.74-7.2 13.49-11.97 23.66-12.04.12.011.23.011.35 0 1.78.011 3.67.16 5.67.471 13.5 2.04 25.27 11.479 36.61 26.859l32.74 59.67.01.03.02.02c8.71 18.19 27.11 38.2 42.7 58.61 15.59 20.401 27.65 40.891 26.08 56.571z'/%3e%3cpath d='m432.77 232.69c-2.63-5.15-8-10.05-17.14-13.8l-.02-.01-.03-.01c-19.01-8.14-27.26-8.72-37.87-15.62-17.27-11.1-31.54-14.99-43.4-14.94-6.21.02-11.76 1.12-16.73 2.84-14.45 4.97-24.04 15.34-30.05 21.03l-.01.01c0 .01-.01.01-.01.02-1.18 1.12-2.7 2.14-6.38 4.84-3.71 2.71-9.27 6.79-17.27 12.79-7.11 5.33-9.42 12.27-6.96 20.4 2.45 8.13 10.29 17.51 24.63 25.62l.02.02.03.01c8.9 5.23 14.98 12.28 21.96 17.89 3.49 2.8 7.16 5.3 11.58 7.19s9.58 3.17 16.04 3.55c15.16.88 26.32-3.67 36.17-9.31 9.87-5.63 18.229-12.52 27.82-15.63l.02-.01.021-.01c19.659-6.14 33.68-18.51 38.069-30.26 2.2-5.88 2.13-11.46-.49-16.61z'/%3e%3c/g%3e%3cpath d='m382.89 261.71c-15.64075 8.1527-33.91 18.04-53.35 18.04-19.43 0-34.78-8.98-45.82-17.73-5.52-4.37-10-8.72-13.38-11.88-5.86434-4.62903-5.16188-11.12168-2.75246-10.93 4.03863.50418 4.64927 5.82168 7.19246 8.2 3.44 3.22 7.75 7.39 12.97 11.53 10.44 8.27 24.36 16.32 41.79 16.32 17.4 0 37.71174-10.21517 50.11-17.17 7.02421-3.94024 15.96224-11.0031 23.25658-16.35742 5.58072-4.09647 5.37722-9.02921 9.98509-8.49216 4.60786.53705 1.19917 5.45984-5.25377 11.09153-6.45295 5.63169-16.54818 13.10397-24.7479 17.37805z' fill='%23202020'/%3e%3cg fill='white'%3e%3cpath d='m622.39 595.47c-2.17-.08-4.31-.069-6.39-.02-.19.01-.38.01-.58.01 5.37-16.96-6.51-29.47-38.17-43.79-32.83-14.439-58.99-13.01-63.41 16.29-.28 1.53-.51 3.1-.68 4.68-2.46.86-4.92 1.94-7.4 3.29-15.41 8.44-23.83 23.74-28.51 42.521-4.67 18.76-6.02 41.43-7.3 66.92v.02c-.79 12.811-6.07 30.15-11.41 48.511-53.78 38.369-128.42 54.989-191.8 11.729-4.29-6.79-9.22-13.52-14.29-20.16-3.24-4.239-6.57-8.45-9.87-12.609 6.5.01 12.03-1.061 16.5-3.08 5.56-2.53 9.46-6.57 11.4-11.771 3.86-10.39-.02-25.05-12.39-41.8-12.37-16.74-33.32-35.63-64.1-54.51 0 0 0 0 0-.01-22.62-14.07-35.26-31.311-41.18-50.03-5.93-18.73-5.1-38.98-.53-58.97 8.76-38.37 31.26-75.69 45.62-99.11 3.86-2.84 1.38 5.28-14.54 34.84-14.26 27.021-40.93 89.38-4.42 138.061.98-34.641 9.25-69.971 23.14-103.021 20.23-45.85 62.54-125.38 65.9-188.76 1.74 1.26 7.69 5.28 10.34 6.79.01.01.01.01.02.01 7.76 4.57 13.59 11.25 21.14 17.32 7.57 6.08 17.02 11.33 31.3 12.16 1.37.08 2.71.12 4.02.12 14.72 0 26.2-4.8 35.76-10.27 10.391-5.94 18.69-12.52 26.561-15.08.01-.01.02-.01.03-.01 16.63-5.2 29.84-14.4 37.359-25.12 12.92 50.92 42.96 124.47 62.271 160.36 10.27 19.04 30.689 59.5 39.51 108.25 5.59-.171 11.75.64 18.34 2.329 23.07-59.81-19.56-124.22-39.06-142.16-7.87-7.64-8.25-11.06-4.34-10.9 21.14 18.71 48.909 56.32 59.01 98.78 4.609 19.359 5.59 39.72.649 59.81 2.41 1 4.87 2.09 7.36 3.271 37.03 18.029 50.72 33.709 44.14 55.109z'/%3e%3cpath d='m434.51 174.03c.08 10.09-1.66 18.68-5.49 27.45-2.18 5-4.689 9.2-7.699 12.84-1.021-.49-2.08-.96-3.181-1.41-3.81-1.63-7.18-2.97-10.199-4.11-3.021-1.14-5.37791-1.91895-7.80891-2.75895 1.761-2.13 5.23-4.64 6.521-7.79 1.96-4.75 2.92-9.39 3.1-14.92 0-.22.07-.41.07-.67.11-5.3-.59-9.83-2.14-14.47-1.62-4.87-3.681-8.37-6.66-11.28-2.99-2.91-5.97-4.23-9.55-4.35-.17-.01-.33-.01-.5-.01-3.36.01-6.28 1.17-9.301 3.69-3.17 2.65-5.52 6.04-7.479 10.76-1.95 4.72-2.91 9.4-3.101 14.96-.029.22-.029.41-.029.63-.07 3.06.13 5.86.6 8.58-6.88-3.43-15.68209-5.93105-21.76209-7.38105-.35-2.63-.55-5.34-.61-8.18v-.77c-.11-10.06 1.54-18.69 5.41-27.45 3.87-8.77 8.66-15.07 15.399-20.2 6.75-5.12 13.381-7.47 21.23-7.55h.37c7.68 0 14.25 2.26 21 7.15 6.85 4.98 11.79 11.2 15.77 19.9 3.9 8.48 5.78 16.77 5.971 26.6-.001.26-.001.48.069.74z'/%3e%3cpath d='m318.43 184.08c-1.01.29-1.99.6-2.96.93-5.5 1.9-9.86686 3.99686-14.08686 6.78686.41-2.92.47-5.88.15-9.19-.03-.18-.03-.33-.03-.51-.44-4.39-1.37-8.07-2.92-11.79-1.65-3.87-3.5-6.6-5.93-8.7-2.2-1.9-4.28-2.78-6.58-2.76-.23 0-.47.01-.71.03-2.58.22-4.72 1.48-6.75 3.95-2.02 2.46-3.35 5.52-4.31 9.58-.96 4.05-1.21 8.03-.81 12.6 0 .18.04.33.04.51.44 4.43 1.33 8.11 2.91 11.83 1.62 3.83 3.5 6.56 5.93 8.66.41.35.81.67 1.21.95-2.52 1.95-4.21314 3.33314-6.29314 4.85314-1.33.97-2.91 2.13-4.75 3.49-4.01-3.76-7.14-8.48-9.88-14.71-3.24-7.36-4.97-14.73-5.49-23.43v-.07c-.48-8.7.37-16.18 2.76-23.92 2.4-7.74 5.6-13.34 10.25-17.94 4.64-4.61 9.32-6.93 14.96-7.22.44-.02.87-.03 1.3-.03 5.11.01 9.67 1.71 14.39 5.48 5.12 4.09 8.99 9.32 12.23 16.69 3.25 7.37 4.98 14.74 5.46 23.44v.07c.23 3.65.2 7.09-.09 10.42z'/%3e%3c/g%3e%3cg fill='%23202020'%3e%3cpath d='m344.08661 204.88969c.64684 2.07624 3.99301 1.73211 5.92629 2.72792 1.69642.87378 3.06086 2.78891 4.96829 2.84398 1.82043.0526 4.65359-.63043 4.89041-2.43615.31286-2.38563-3.17083-3.90165-5.41255-4.77563-2.88469-1.12471-6.58056-1.69534-9.28659-.19073-.6201.34477-1.29689 1.15328-1.08585 1.83064z'/%3e%3cpath d='m324.32226 204.88969c-.64684 2.07624-3.99301 1.73211-5.92629 2.72792-1.69642.87378-3.06086 2.78891-4.96829 2.84398-1.82043.0526-4.65359-.63043-4.89041-2.43615-.31286-2.38563 3.17083-3.90165 5.41255-4.77563 2.88469-1.12471 6.58056-1.69534 9.28659-.19073.6201.34477 1.29689 1.15328 1.08585 1.83064z'/%3e%3c/g%3e%3c/svg%3e"}c.style.width="20px",c.style.height="20px",i.appendChild(c);const e=document.createElement("div");e.textContent=function(a){const r=a.toUpperCase().split("").map((a=>127462+a.charCodeAt(0)-"A".charCodeAt(0)));return String.fromCodePoint(...r)}(r.country),e.style.position="relative",e.style.left="-2px",i.appendChild(e);const n=i.clientWidth,t=i.clientHeight;return i.style.left=`${P(r.posX,n)}px`,i.style.top=`${C(r.posY,t)}px`,document.body.appendChild(i),i}r(`${o.startsWith("https")?"wss":"ws"}://${t}/subscribe?url=${o}&country=${e}&os=${i}`,h,(function(a){let r=document.querySelector(`[data-cursor-id="${a.id}"]`);if(!r)return;const i=r.clientWidth,c=r.clientHeight;r.style.left=`${P(a.posX,i)}px`,r.style.top=`${C(a.posY,c)}px`,0===a.posX&&0===a.posY?r.style.display="none":r.style.display="flex"}),(function(a){const r=document.querySelector(`[data-cursor-id="${a.id}"]`);r&&r.remove()}))})()}(); -------------------------------------------------------------------------------- /local-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Encursors Demo 7 | 8 | 9 | 10 |
11 |

Encursors Local Demo

12 | 13 |

14 | Stopping by Woods on a Snowy Evening, by Robert Frost 15 |

16 |

17 | Whose woods these are I think I know. 18 | His house is in the village though; 19 | He will not see me stopping here 20 | To watch his woods fill up with snow. 21 | 22 | My little horse must think it queer 23 | To stop without a farmhouse near 24 | Between the woods and frozen lake 25 | The darkest evening of the year. 26 | 27 | He gives his harness bells a shake 28 | To ask if there is some mistake. 29 | The only other sound’s the sweep 30 | Of easy wind and downy flake. 31 | 32 | The woods are lovely, dark and deep, 33 | But I have promises to keep, 34 | And miles to go before I sleep, 35 | And miles to go before I sleep. 36 |

37 | 38 |

39 | Lines Written in Early Spring, by William Wordsworth 40 |

41 |

42 | I heard a thousand blended notes, 43 | While in a grove I sate reclined, 44 | In that sweet mood when pleasant thoughts 45 | Bring sad thoughts to the mind. 46 | 47 | To her fair works did Nature link 48 | The human soul that through me ran; 49 | And much it grieved my heart to think 50 | What man has made of man. 51 | 52 | Through primrose tufts, in that green bower, 53 | The periwinkle trailed its wreaths; 54 | And ’tis my faith that every flower 55 | Enjoys the air it breathes. 56 | 57 | The birds around me hopped and played, 58 | Their thoughts I cannot measure:— 59 | But the least motion which they made 60 | It seemed a thrill of pleasure. 61 | 62 | The budding twigs spread out their fan, 63 | To catch the breezy air; 64 | And I must think, do all I can, 65 | That there was pleasure there. 66 | 67 | If this belief from heaven be sent, 68 | If such be Nature’s holy plan, 69 | Have I not reason to lament 70 | What man has made of man? 71 |

72 | 73 |

74 | The Summer Day, by Mary Oliver 75 |

76 |

77 | Who made the world? 78 | Who made the swan, and the black bear? 79 | Who made the grasshopper? 80 | This grasshopper, I mean— 81 | the one who has flung herself out of the grass, 82 | the one who is eating sugar out of my hand, 83 | who is moving her jaws back and forth instead of up and down— 84 | who is gazing around with her enormous and complicated eyes. 85 | Now she lifts her pale forearms and thoroughly washes her face. 86 | Now she snaps her wings open, and floats away. 87 | I don't know exactly what a prayer is. 88 | I do know how to pay attention, how to fall down 89 | into the grass, how to kneel down in the grass, 90 | how to be idle and blessed, how to stroll through the fields, 91 | which is what I have been doing all day. 92 | Tell me, what else should I have done? 93 | Doesn't everything die at last, and too soon? 94 | Tell me, what is it you plan to do 95 | with your one wild and precious life? 96 |

97 | 98 |

99 | To Autumn, by John Keats 100 |

101 |

102 | Season of mists and mellow fruitfulness, 103 | Close bosom-friend of the maturing sun; 104 | Conspiring with him how to load and bless 105 | With fruit the vines that round the thatch-eves run; 106 | To bend with apples the moss’d cottage-trees, 107 | And fill all fruit with ripeness to the core; 108 | To swell the gourd, and plump the hazel shells 109 | With a sweet kernel; to set budding more, 110 | And still more, later flowers for the bees, 111 | Until they think warm days will never cease, 112 | For summer has o’er-brimm’d their clammy cells. 113 | 114 | Who hath not seen thee oft amid thy store? 115 | Sometimes whoever seeks abroad may find 116 | Thee sitting careless on a granary floor, 117 | Thy hair soft-lifted by the winnowing wind; 118 | Or on a half-reap’d furrow sound asleep, 119 | Drowsed with the fume of poppies, while thy hook 120 | Spares the next swath and all its twined flowers: 121 | And sometimes like a gleaner thou dost keep 122 | Steady thy laden head across a brook; 123 | Or by a cider-press, with patient look, 124 | Thou watchest the last oozings, hours by hours. 125 | 126 | Where are the songs of Spring? Ay, where are they? 127 | Think not of them, thou hast thy music too,— 128 | While barred clouds bloom the soft-dying day, 129 | And touch the stubble-plains with rosy hue; 130 | Then in a wailful choir the small gnats mourn 131 | Among the river sallows, borne aloft 132 | Or sinking as the light wind lives or dies; 133 | And full-grown lambs loud bleat from hilly bourn; 134 | Hedge-crickets sing; and now with treble soft 135 | The redbreast whistles from a garden-croft, 136 | And gathering swallows twitter in the skies. 137 |

138 |
139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /local-demo/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #f0f0f0; 3 | font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif; 4 | font-weight: normal; 5 | } 6 | 7 | main { 8 | max-width: 600px; 9 | margin: 0 auto; 10 | } 11 | 12 | p { 13 | white-space: pre-line; 14 | margin-top: 0; 15 | margin-bottom: 3em; 16 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anfragment/encursors/7d22976819ffaecab1546e5fb694bb0dbf73e6da/logo.png -------------------------------------------------------------------------------- /script/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/* -------------------------------------------------------------------------------- /script/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "always", 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "printWidth": 100, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /script/dist/cursors.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var a="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='10 7.4 11.4 18.1'%3e%3cg fill='none' fill-rule='evenodd' transform='translate(10 7)'%3e%3cpath d='m6.148 18.473 1.863-1.003 1.615-.839-2.568-4.816h4.332l-11.379-11.408v16.015l3.316-3.221z' fill='white'/%3e%3cpath d='m6.431 17 1.765-.941-2.775-5.202h3.604l-8.025-8.043v11.188l2.53-2.442z' fill='black'/%3e%3c/g%3e%3c/svg%3e";function r(a,r,i,c){let e=new WebSocket(a),n=!1,t=0,u=0;const o=function(a,r){let i,c,e=0;return function(...n){if(c=n,i)return;const t=Date.now();if(t-e>r)return e=t,void a(...c);i=setTimeout((()=>{e=Date.now(),a(...c),i=null}),r-(t-e))}}((a=>{if("number"==typeof a.clientX&&(t=a.clientX,u=a.clientY),!n)return;const r=Math.floor(t+window.scrollX),i=Math.floor(u+window.scrollY);e.send(JSON.stringify([r,i]))}),2e3);document.addEventListener("mousemove",o),document.addEventListener("mouseenter",o),document.addEventListener("scroll",o),e.onopen=function(){n=!0},e.onclose=function(){n=!1,setTimeout((()=>{e=new WebSocket(a)}),5e3)},e.onmessage=function(a){const e=JSON.parse(a.data);switch(e.type){case"enter":r(e.payload);break;case"move":i(e.payload);break;case"leave":c(e.payload)}},e.onerror=function(a){console.error("WebSocket error:",a)}}function i(a,r){var i=Object.keys(a);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(a);r&&(c=c.filter((function(r){return Object.getOwnPropertyDescriptor(a,r).enumerable}))),i.push.apply(i,c)}return i}function c(a){for(var r=1;r=0||(e[i]=a[i]);return e}(a,r);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(a);for(c=0;c=0||Object.prototype.propertyIsEnumerable.call(a,i)&&(e[i]=a[i])}return e}function t(a,r){return function(a){if(Array.isArray(a))return a}(a)||function(a,r){var i=null==a?null:"undefined"!=typeof Symbol&&a[Symbol.iterator]||a["@@iterator"];if(null!=i){var c,e,n,t,u=[],o=!0,A=!1;try{if(n=(i=i.call(a)).next,0===r);else for(;!(o=(c=n.call(i)).done)&&(u.push(c.value),u.length!==r);o=!0);}catch(a){A=!0,e=a}finally{try{if(!o&&null!=i.return&&(t=i.return(),Object(t)!==t))return}finally{if(A)throw e}}return u}}(a,r)||function(a,r){if(!a)return;if("string"==typeof a)return u(a,r);var i=Object.prototype.toString.call(a).slice(8,-1);"Object"===i&&a.constructor&&(i=a.constructor.name);if("Map"===i||"Set"===i)return Array.from(a);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return u(a,r)}(a,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(a,r){(null==r||r>a.length)&&(r=a.length);for(var i=0,c=new Array(r);i1&&void 0!==arguments[1]?arguments[1]:{};return p[a]||function(a){if(!a)return;p[a.id]=a}(s(A,a)),function(a,r){if(!a)return null;var i=r||{},e=i.deprecated;a.allTimezones;var t=n(a,m),u=e?a.allTimezones:a.timezones;return c(c({},t),{},{timezones:u})}(p[a],r)}function v(a){return f[a]||function(a){if(!a)return;f[a.name]=a,M=Object.keys(a).length}(function(a,r){var i=a.timezones[r];if(!i)return null;var e=i.a,n=void 0===e?null:e,t=c(c({},n?a.timezones[n]:{}),a.timezones[r]),u=t.c||[],o=t.u,A=Number.isInteger(t.d)?t.d:o,s={name:r,countries:u,utcOffset:o,utcOffsetStr:h(o),dstOffset:A,dstOffsetStr:h(A),aliasOf:n};return i.r&&(s.deprecated=!0),s}(A,a)),f[a]?c({},f[a]):null}function S(a){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return((v(a)||{}).countries||[]).map((function(a){return g(a,r)}))}var E={getCountry:g,getTimezone:v,getAllCountries:function(){var a=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return Object.keys(A.countries).reduce((function(r,i){return Object.assign(r,e({},i,g(i,a)))}),{})},getAllTimezones:function(){var a=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return l!==M&&Object.keys(A.timezones).forEach(v),function(a,r){var i=r||{};return!0===i.deprecated?a:Object.keys(a).reduce((function(r,i){return a[i].deprecated||Object.assign(r,e({},i,a[i])),r}),{})}(f,a)},getTimezonesForCountry:function(a){var r=g(a,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{});return r?(r.timezones||[]).map(v):null},getCountriesForTimezone:S,getCountryForTimezone:function(a){return t(S(a,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}),1)[0]||null}};function C(a,r){return Math.min(Math.max(0,a),document.documentElement.scrollHeight-1.5*r-1)}function P(a,r){return Math.min(Math.max(0,a),document.documentElement.scrollWidth-1.5*r-1)}(async()=>{console.log("Collaborative cursors experience on this page is powered by Encursors. Learn more at:\nhttps://github.com/anfragment/encursors");if(!0===window.matchMedia("(prefers-reduced-motion: reduce)")||!0===window.matchMedia("(prefers-reduced-motion: reduce)").matches)return void console.debug("Reduced motion is enabled, not showing cursors");const i=function(){const a=window.navigator?.userAgentData?.platform||window.navigator.platform,r=["Win32","Win64","Windows","WinCE"];if(-1!==["macOS","Macintosh","MacIntel","MacPPC","Mac68K"].indexOf(a))return"mac";if(-1!==r.indexOf(a))return"win";if(/Linux/.test(a))return"linux";return null}();if(!i)return void console.debug("Unsupported OS, not showing cursors");const c=E.getCountryForTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);if(!c)return void console.debug("Could not determine country, not showing cursors");const e=c.id,n=document.currentScript,t=n.getAttribute("data-api-url")||"prod-encursors-ypdi.encr.app",u=n.getAttribute("data-z-index"),o=window.location.href.split("?")[0].split("#")[0],A=await fetch(`${o.startsWith("https")?"https":"http"}://${t}/cursors?url=${o}`),s=await A.json();for(const a of s.cursors||[])h(a);function h(r){const i=document.createElement("div");i.setAttribute("data-cursor-id",r.id),i.style.position="absolute",u&&(i.style.zIndex=u),i.style.transition="left 0.2s, top 0.2s",0===r.posX&&0===r.posY?i.style.display="none":i.style.display="flex",i.style.alignItems="center",i.style.justifyContent="center",i.style.pointerEvents="none";const c=document.createElement("img");switch(r.os){case 0:default:c.src=a;break;case 1:c.src="data:image/svg+xml,%3csvg viewBox='0 0 492 779' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m41 41h41v41h-41z'/%3e%3cpath d='m82 82h41v41h-41z'/%3e%3cpath d='m123 123h41v41h-41z'/%3e%3cpath d='m164 164h41v41h-41z'/%3e%3cpath d='m205 205h41v41h-41z'/%3e%3cpath d='m246 246h41v41h-41z'/%3e%3cpath d='m287 287h41v41h-41z'/%3e%3cpath d='m328 328h41v41h-41z'/%3e%3cpath d='m369 369h41v41h-41z'/%3e%3cpath d='m410 410h41v41h-41z'/%3e%3cpath d='m451.01 451h41v41h-41z'/%3e%3cpath d='m451.01 492h41v41h-41z'/%3e%3cpath d='m410 492h41v41h-41z'/%3e%3cpath d='m369 492h41v41h-41z'/%3e%3cpath d='m328 492h41v41h-41z'/%3e%3cpath d='m287 492h41v41h-41z'/%3e%3cpath d='m287 533h41v41h-41z'/%3e%3cpath d='m328 574h41v41h-41z'/%3e%3cpath d='m328 615h41v41h-41z'/%3e%3cpath d='m369 656.03h41v41h-41z'/%3e%3cpath d='m369 697.03h41v41h-41z'/%3e%3cpath d='m328 738.03h41v41h-41z'/%3e%3cpath d='m287 738.03h41v41h-41z'/%3e%3cpath d='m246 697.03h41v41h-41z'/%3e%3cpath d='m246 656.03h41v41h-41z'/%3e%3cpath d='m205 615h41v41h-41z'/%3e%3cpath d='m205 574h41v41h-41z'/%3e%3cpath d='m164 533h41v41h-41z'/%3e%3cpath d='m123 574h41v41h-41z'/%3e%3cpath d='m82 615h41v41h-41z'/%3e%3cpath d='m41 656.03h41v41h-41z'/%3e%3cpath d='m0 656.03h41v41h-41z'/%3e%3cpath d='m0 615h41v41h-41z'/%3e%3cpath d='m0 574h41v41h-41z'/%3e%3cpath d='m0 533h41v41h-41z'/%3e%3cpath d='m0 492h41v41h-41z'/%3e%3cpath d='m0 451h41v41h-41z'/%3e%3cpath d='m0 410h41v41h-41z'/%3e%3cpath d='m0 369h41v41h-41z'/%3e%3cpath d='m0 328h41v41h-41z'/%3e%3cpath d='m0 287h41v41h-41z'/%3e%3cpath d='m0 246h41v41h-41z'/%3e%3cpath d='m0 205h41v41h-41z'/%3e%3cpath d='m0 164h41v41h-41z'/%3e%3cpath d='m0 123h41v41h-41z'/%3e%3cpath d='m0 82h41v41h-41z'/%3e%3cpath d='m0 41h41v41h-41z'/%3e%3cpath d='m0 0h41v41h-41z'/%3e%3cg fill='white'%3e%3cpath d='m41 82h41v41h-41z'/%3e%3cpath d='m82 123h41v41h-41z'/%3e%3cpath d='m41 123h41v41h-41z'/%3e%3cpath d='m41 164h41v41h-41z'/%3e%3cpath d='m82 164h41v41h-41z'/%3e%3cpath d='m123 164h41v41h-41z'/%3e%3cpath d='m41 205h41v41h-41z'/%3e%3cpath d='m82 205h41v41h-41z'/%3e%3cpath d='m123 205h41v41h-41z'/%3e%3cpath d='m164 205h41v41h-41z'/%3e%3cpath d='m41 615h41v41h-41z'/%3e%3cpath d='m41 574h41v41h-41z'/%3e%3cpath d='m82 574h41v41h-41z'/%3e%3cpath d='m246 574h41v41h-41z'/%3e%3cpath d='m287 615h41v41h-41z'/%3e%3cpath d='m246 615h41v41h-41z'/%3e%3cpath d='m287 574h41v41h-41z'/%3e%3cpath d='m287 656.03h41v41h-41z'/%3e%3cpath d='m328 656.03h41v41h-41z'/%3e%3cpath d='m287 697.03h41v41h-41z'/%3e%3cpath d='m328 697.03h41v41h-41z'/%3e%3cpath d='m205 533h41v41h-41z'/%3e%3cpath d='m246 533h41v41h-41z'/%3e%3cpath d='m123 533h41v41h-41z'/%3e%3cpath d='m82 533h41v41h-41z'/%3e%3cpath d='m41 533h41v41h-41z'/%3e%3cpath d='m41 492h41v41h-41z'/%3e%3cpath d='m82 492h41v41h-41z'/%3e%3cpath d='m123 492h41v41h-41z'/%3e%3cpath d='m164 492h41v41h-41z'/%3e%3cpath d='m205 492h41v41h-41z'/%3e%3cpath d='m246 492h41v41h-41z'/%3e%3cpath d='m410 451h41v41h-41z'/%3e%3cpath d='m369 451h41v41h-41z'/%3e%3cpath d='m328 451h41v41h-41z'/%3e%3cpath d='m287 451h41v41h-41z'/%3e%3cpath d='m246 451h41v41h-41z'/%3e%3cpath d='m205 451h41v41h-41z'/%3e%3cpath d='m164 451h41v41h-41z'/%3e%3cpath d='m123 451h41v41h-41z'/%3e%3cpath d='m82 451h41v41h-41z'/%3e%3cpath d='m41 451h41v41h-41z'/%3e%3cpath d='m41 410h41v41h-41z'/%3e%3cpath d='m82 410h41v41h-41z'/%3e%3cpath d='m41 369h41v41h-41z'/%3e%3cpath d='m41 328h41v41h-41z'/%3e%3cpath d='m82 369h41v41h-41z'/%3e%3cpath d='m123 410h41v41h-41z'/%3e%3cpath d='m164 410h41v41h-41z'/%3e%3cpath d='m123 369h41v41h-41z'/%3e%3cpath d='m82 328h41v41h-41z'/%3e%3cpath d='m41 287h41v41h-41z'/%3e%3cpath d='m41 246h41v41h-41z'/%3e%3cpath d='m82 287h41v41h-41z'/%3e%3cpath d='m123 328h41v41h-41z'/%3e%3cpath d='m164 369h41v41h-41z'/%3e%3cpath d='m205 410h41v41h-41z'/%3e%3cpath d='m82 246h41v41h-41z'/%3e%3cpath d='m123 287h41v41h-41z'/%3e%3cpath d='m164 328h41v41h-41z'/%3e%3cpath d='m205 369h41v41h-41z'/%3e%3cpath d='m246 410h41v41h-41z'/%3e%3cpath d='m287 410h41v41h-41z'/%3e%3cpath d='m328 410h41v41h-41z'/%3e%3cpath d='m369 410h41v41h-41z'/%3e%3cpath d='m328 369h41v41h-41z'/%3e%3cpath d='m287 328h41v41h-41z'/%3e%3cpath d='m246 287h41v41h-41z'/%3e%3cpath d='m205 246h41v41h-41z'/%3e%3cpath d='m164 246h41v41h-41z'/%3e%3cpath d='m205 287h41v41h-41z'/%3e%3cpath d='m246 328h41v41h-41z'/%3e%3cpath d='m287 369h41v41h-41z'/%3e%3cpath d='m246 369h41v41h-41z'/%3e%3cpath d='m205 328h41v41h-41z'/%3e%3cpath d='m164 287h41v41h-41z'/%3e%3cpath d='m123 246h41v41h-41z'/%3e%3c/g%3e%3c/svg%3e";break;case 2:c.src="data:image/svg+xml,%3csvg enable-background='new 0 0 712 860' height='860' viewBox='0 0 712 860' width='712' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m683.91 677.74c-.01-.01-.021-.021-.021-.03-6.069-6.85-8.96-19.55-12.069-33.08-3.101-13.52-6.57-28.1-17.67-37.55-.021-.021-.051-.04-.07-.061-2.2-1.92-4.46-3.539-6.73-4.89-2.279-1.35-4.59-2.45-6.899-3.32 15.43-45.76 9.38-91.329-6.2-132.5-19.11-50.54-52.48-94.569-77.97-124.689-28.53-35.99-56.431-70.15-55.88-120.61.849-77.01 8.469-219.82-127.051-220.01-5.51-.01-11.26.22-17.25.7-151.44 12.19-111.27 172.19-113.52 225.76-2.77 39.18-10.71 70.06-37.66 108.36-31.65 37.64-76.23 98.57-97.34 162-9.96 29.93-14.7 60.439-10.33 89.319-1.37 1.23-2.67 2.521-3.92 3.841-9.29 9.93-16.16 21.949-23.81 30.039-7.15 7.141-17.33 9.851-28.53 13.86-11.2 4.021-23.49 9.94-30.95 24.26 0 0 0 .011-.01.011-.01.02-.02.05-.04.069-3.51 6.55-4.65 13.62-4.65 20.79 0 6.63.98 13.351 1.97 19.82 2.06 13.46 4.15 26.189 1.38 34.81-8.86 24.23-10 40.98-3.76 53.141 6.26 12.18 19.11 17.55 33.64 20.59 29.06 6.06 68.41 4.56 99.42 21l2.67-5.03-2.64 5.04c33.2 17.36 66.86 23.521 93.71 17.39 19.48-4.439 35.28-16.039 43.4-33.88 21-.1 44.05-9 80.97-11.029 25.05-2.021 56.34 8.899 92.33 6.899.94 3.9 2.301 7.66 4.16 11.23.021.029.04.069.061.1 13.949 27.9 39.869 40.66 67.5 38.48 27.659-2.181 57.069-18.49 80.85-46.78l-4.36-3.66 4.391 3.62c22.66-27.48 60.27-38.87 85.22-53.91 12.47-7.52 22.58-16.939 23.37-30.62.778-13.67-7.252-28.99-25.712-49.48z' fill='%23202020'/%3e%3cg fill='white'%3e%3cpath d='m698.24 726.57c-.48 8.439-6.59 14.71-17.88 21.51-22.561 13.61-62.551 25.45-88.08 56.38-22.181 26.39-49.221 40.88-73.03 42.76s-44.35-8-56.47-32.31l-.021-.05-.029-.061c-7.521-14.3-4.391-36.85 1.939-60.649 6.33-23.79 15.43-48.23 16.65-68.08v-.061c1.279-25.439 2.71-47.66 6.979-64.81 4.28-17.15 11.01-28.75 22.931-35.28.56-.3 1.109-.59 1.659-.86 1.351 22.03 12.261 44.511 31.54 49.37 21.101 5.561 51.511-12.54 64.351-27.31 2.569-.101 5.069-.23 7.5-.29 11.27-.271 20.71.38 30.37 8.82l.029.029.03.021c7.42 6.29 10.95 18.17 14.01 31.47 3.061 13.311 5.5 27.8 14.681 38.13l.01.01.01.021c17.641 19.57 23.311 32.79 22.821 41.24z'/%3e%3cpath d='m269.49 788.95-.01.06v.07c-2.04 26.74-17.12 41.3-40.28 46.59-23.14 5.29-54.53.021-85.87-16.37-.01 0-.02-.01-.03-.01-34.68-18.37-75.93-16.54-102.4-22.07-13.23-2.76-21.87-6.92-25.83-14.64-3.96-7.73-4.05-21.2 4.37-44.17l.04-.101.03-.1c4.17-12.85 1.08-26.91-.94-40.11-2.02-13.189-3.01-25.199 1.5-33.56l.04-.08c5.77-11.12 14.23-15.1 24.73-18.86 10.51-3.77 22.96-6.729 32.79-16.59l.06-.05.05-.05c9.09-9.59 15.92-21.62 23.91-30.15 6.74-7.2 13.49-11.97 23.66-12.04.12.011.23.011.35 0 1.78.011 3.67.16 5.67.471 13.5 2.04 25.27 11.479 36.61 26.859l32.74 59.67.01.03.02.02c8.71 18.19 27.11 38.2 42.7 58.61 15.59 20.401 27.65 40.891 26.08 56.571z'/%3e%3cpath d='m432.77 232.69c-2.63-5.15-8-10.05-17.14-13.8l-.02-.01-.03-.01c-19.01-8.14-27.26-8.72-37.87-15.62-17.27-11.1-31.54-14.99-43.4-14.94-6.21.02-11.76 1.12-16.73 2.84-14.45 4.97-24.04 15.34-30.05 21.03l-.01.01c0 .01-.01.01-.01.02-1.18 1.12-2.7 2.14-6.38 4.84-3.71 2.71-9.27 6.79-17.27 12.79-7.11 5.33-9.42 12.27-6.96 20.4 2.45 8.13 10.29 17.51 24.63 25.62l.02.02.03.01c8.9 5.23 14.98 12.28 21.96 17.89 3.49 2.8 7.16 5.3 11.58 7.19s9.58 3.17 16.04 3.55c15.16.88 26.32-3.67 36.17-9.31 9.87-5.63 18.229-12.52 27.82-15.63l.02-.01.021-.01c19.659-6.14 33.68-18.51 38.069-30.26 2.2-5.88 2.13-11.46-.49-16.61z'/%3e%3c/g%3e%3cpath d='m382.89 261.71c-15.64075 8.1527-33.91 18.04-53.35 18.04-19.43 0-34.78-8.98-45.82-17.73-5.52-4.37-10-8.72-13.38-11.88-5.86434-4.62903-5.16188-11.12168-2.75246-10.93 4.03863.50418 4.64927 5.82168 7.19246 8.2 3.44 3.22 7.75 7.39 12.97 11.53 10.44 8.27 24.36 16.32 41.79 16.32 17.4 0 37.71174-10.21517 50.11-17.17 7.02421-3.94024 15.96224-11.0031 23.25658-16.35742 5.58072-4.09647 5.37722-9.02921 9.98509-8.49216 4.60786.53705 1.19917 5.45984-5.25377 11.09153-6.45295 5.63169-16.54818 13.10397-24.7479 17.37805z' fill='%23202020'/%3e%3cg fill='white'%3e%3cpath d='m622.39 595.47c-2.17-.08-4.31-.069-6.39-.02-.19.01-.38.01-.58.01 5.37-16.96-6.51-29.47-38.17-43.79-32.83-14.439-58.99-13.01-63.41 16.29-.28 1.53-.51 3.1-.68 4.68-2.46.86-4.92 1.94-7.4 3.29-15.41 8.44-23.83 23.74-28.51 42.521-4.67 18.76-6.02 41.43-7.3 66.92v.02c-.79 12.811-6.07 30.15-11.41 48.511-53.78 38.369-128.42 54.989-191.8 11.729-4.29-6.79-9.22-13.52-14.29-20.16-3.24-4.239-6.57-8.45-9.87-12.609 6.5.01 12.03-1.061 16.5-3.08 5.56-2.53 9.46-6.57 11.4-11.771 3.86-10.39-.02-25.05-12.39-41.8-12.37-16.74-33.32-35.63-64.1-54.51 0 0 0 0 0-.01-22.62-14.07-35.26-31.311-41.18-50.03-5.93-18.73-5.1-38.98-.53-58.97 8.76-38.37 31.26-75.69 45.62-99.11 3.86-2.84 1.38 5.28-14.54 34.84-14.26 27.021-40.93 89.38-4.42 138.061.98-34.641 9.25-69.971 23.14-103.021 20.23-45.85 62.54-125.38 65.9-188.76 1.74 1.26 7.69 5.28 10.34 6.79.01.01.01.01.02.01 7.76 4.57 13.59 11.25 21.14 17.32 7.57 6.08 17.02 11.33 31.3 12.16 1.37.08 2.71.12 4.02.12 14.72 0 26.2-4.8 35.76-10.27 10.391-5.94 18.69-12.52 26.561-15.08.01-.01.02-.01.03-.01 16.63-5.2 29.84-14.4 37.359-25.12 12.92 50.92 42.96 124.47 62.271 160.36 10.27 19.04 30.689 59.5 39.51 108.25 5.59-.171 11.75.64 18.34 2.329 23.07-59.81-19.56-124.22-39.06-142.16-7.87-7.64-8.25-11.06-4.34-10.9 21.14 18.71 48.909 56.32 59.01 98.78 4.609 19.359 5.59 39.72.649 59.81 2.41 1 4.87 2.09 7.36 3.271 37.03 18.029 50.72 33.709 44.14 55.109z'/%3e%3cpath d='m434.51 174.03c.08 10.09-1.66 18.68-5.49 27.45-2.18 5-4.689 9.2-7.699 12.84-1.021-.49-2.08-.96-3.181-1.41-3.81-1.63-7.18-2.97-10.199-4.11-3.021-1.14-5.37791-1.91895-7.80891-2.75895 1.761-2.13 5.23-4.64 6.521-7.79 1.96-4.75 2.92-9.39 3.1-14.92 0-.22.07-.41.07-.67.11-5.3-.59-9.83-2.14-14.47-1.62-4.87-3.681-8.37-6.66-11.28-2.99-2.91-5.97-4.23-9.55-4.35-.17-.01-.33-.01-.5-.01-3.36.01-6.28 1.17-9.301 3.69-3.17 2.65-5.52 6.04-7.479 10.76-1.95 4.72-2.91 9.4-3.101 14.96-.029.22-.029.41-.029.63-.07 3.06.13 5.86.6 8.58-6.88-3.43-15.68209-5.93105-21.76209-7.38105-.35-2.63-.55-5.34-.61-8.18v-.77c-.11-10.06 1.54-18.69 5.41-27.45 3.87-8.77 8.66-15.07 15.399-20.2 6.75-5.12 13.381-7.47 21.23-7.55h.37c7.68 0 14.25 2.26 21 7.15 6.85 4.98 11.79 11.2 15.77 19.9 3.9 8.48 5.78 16.77 5.971 26.6-.001.26-.001.48.069.74z'/%3e%3cpath d='m318.43 184.08c-1.01.29-1.99.6-2.96.93-5.5 1.9-9.86686 3.99686-14.08686 6.78686.41-2.92.47-5.88.15-9.19-.03-.18-.03-.33-.03-.51-.44-4.39-1.37-8.07-2.92-11.79-1.65-3.87-3.5-6.6-5.93-8.7-2.2-1.9-4.28-2.78-6.58-2.76-.23 0-.47.01-.71.03-2.58.22-4.72 1.48-6.75 3.95-2.02 2.46-3.35 5.52-4.31 9.58-.96 4.05-1.21 8.03-.81 12.6 0 .18.04.33.04.51.44 4.43 1.33 8.11 2.91 11.83 1.62 3.83 3.5 6.56 5.93 8.66.41.35.81.67 1.21.95-2.52 1.95-4.21314 3.33314-6.29314 4.85314-1.33.97-2.91 2.13-4.75 3.49-4.01-3.76-7.14-8.48-9.88-14.71-3.24-7.36-4.97-14.73-5.49-23.43v-.07c-.48-8.7.37-16.18 2.76-23.92 2.4-7.74 5.6-13.34 10.25-17.94 4.64-4.61 9.32-6.93 14.96-7.22.44-.02.87-.03 1.3-.03 5.11.01 9.67 1.71 14.39 5.48 5.12 4.09 8.99 9.32 12.23 16.69 3.25 7.37 4.98 14.74 5.46 23.44v.07c.23 3.65.2 7.09-.09 10.42z'/%3e%3c/g%3e%3cg fill='%23202020'%3e%3cpath d='m344.08661 204.88969c.64684 2.07624 3.99301 1.73211 5.92629 2.72792 1.69642.87378 3.06086 2.78891 4.96829 2.84398 1.82043.0526 4.65359-.63043 4.89041-2.43615.31286-2.38563-3.17083-3.90165-5.41255-4.77563-2.88469-1.12471-6.58056-1.69534-9.28659-.19073-.6201.34477-1.29689 1.15328-1.08585 1.83064z'/%3e%3cpath d='m324.32226 204.88969c-.64684 2.07624-3.99301 1.73211-5.92629 2.72792-1.69642.87378-3.06086 2.78891-4.96829 2.84398-1.82043.0526-4.65359-.63043-4.89041-2.43615-.31286-2.38563 3.17083-3.90165 5.41255-4.77563 2.88469-1.12471 6.58056-1.69534 9.28659-.19073.6201.34477 1.29689 1.15328 1.08585 1.83064z'/%3e%3c/g%3e%3c/svg%3e"}c.style.width="20px",c.style.height="20px",i.appendChild(c);const e=document.createElement("div");e.textContent=function(a){const r=a.toUpperCase().split("").map((a=>127462+a.charCodeAt(0)-"A".charCodeAt(0)));return String.fromCodePoint(...r)}(r.country),e.style.position="relative",e.style.left="-2px",i.appendChild(e);const n=i.clientWidth,t=i.clientHeight;return i.style.left=`${P(r.posX,n)}px`,i.style.top=`${C(r.posY,t)}px`,document.body.appendChild(i),i}r(`${o.startsWith("https")?"wss":"ws"}://${t}/subscribe?url=${o}&country=${e}&os=${i}`,h,(function(a){let r=document.querySelector(`[data-cursor-id="${a.id}"]`);if(!r)return;const i=r.clientWidth,c=r.clientHeight;r.style.left=`${P(a.posX,i)}px`,r.style.top=`${C(a.posY,c)}px`,0===a.posX&&0===a.posY?r.style.display="none":r.style.display="flex"}),(function(a){const r=document.querySelector(`[data-cursor-id="${a.id}"]`);r&&r.remove()}))})()}(); -------------------------------------------------------------------------------- /script/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "script", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@rollup/plugin-image": "^3.0.3", 13 | "@rollup/plugin-node-resolve": "^15.2.3", 14 | "countries-and-timezones": "^3.6.0", 15 | "prettier": "^3.3.1", 16 | "rollup": "^4.17.2", 17 | "terser": "^5.31.0" 18 | } 19 | }, 20 | "node_modules/@jridgewell/gen-mapping": { 21 | "version": "0.3.5", 22 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", 23 | "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", 24 | "dependencies": { 25 | "@jridgewell/set-array": "^1.2.1", 26 | "@jridgewell/sourcemap-codec": "^1.4.10", 27 | "@jridgewell/trace-mapping": "^0.3.24" 28 | }, 29 | "engines": { 30 | "node": ">=6.0.0" 31 | } 32 | }, 33 | "node_modules/@jridgewell/resolve-uri": { 34 | "version": "3.1.2", 35 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 36 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 37 | "engines": { 38 | "node": ">=6.0.0" 39 | } 40 | }, 41 | "node_modules/@jridgewell/set-array": { 42 | "version": "1.2.1", 43 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 44 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 45 | "engines": { 46 | "node": ">=6.0.0" 47 | } 48 | }, 49 | "node_modules/@jridgewell/source-map": { 50 | "version": "0.3.6", 51 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", 52 | "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", 53 | "dependencies": { 54 | "@jridgewell/gen-mapping": "^0.3.5", 55 | "@jridgewell/trace-mapping": "^0.3.25" 56 | } 57 | }, 58 | "node_modules/@jridgewell/sourcemap-codec": { 59 | "version": "1.4.15", 60 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 61 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" 62 | }, 63 | "node_modules/@jridgewell/trace-mapping": { 64 | "version": "0.3.25", 65 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 66 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 67 | "dependencies": { 68 | "@jridgewell/resolve-uri": "^3.1.0", 69 | "@jridgewell/sourcemap-codec": "^1.4.14" 70 | } 71 | }, 72 | "node_modules/@rollup/plugin-image": { 73 | "version": "3.0.3", 74 | "resolved": "https://registry.npmjs.org/@rollup/plugin-image/-/plugin-image-3.0.3.tgz", 75 | "integrity": "sha512-qXWQwsXpvD4trSb8PeFPFajp8JLpRtqqOeNYRUKnEQNHm7e5UP7fuSRcbjQAJ7wDZBbnJvSdY5ujNBQd9B1iFg==", 76 | "dependencies": { 77 | "@rollup/pluginutils": "^5.0.1", 78 | "mini-svg-data-uri": "^1.4.4" 79 | }, 80 | "engines": { 81 | "node": ">=14.0.0" 82 | }, 83 | "peerDependencies": { 84 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 85 | }, 86 | "peerDependenciesMeta": { 87 | "rollup": { 88 | "optional": true 89 | } 90 | } 91 | }, 92 | "node_modules/@rollup/plugin-node-resolve": { 93 | "version": "15.2.3", 94 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", 95 | "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", 96 | "dependencies": { 97 | "@rollup/pluginutils": "^5.0.1", 98 | "@types/resolve": "1.20.2", 99 | "deepmerge": "^4.2.2", 100 | "is-builtin-module": "^3.2.1", 101 | "is-module": "^1.0.0", 102 | "resolve": "^1.22.1" 103 | }, 104 | "engines": { 105 | "node": ">=14.0.0" 106 | }, 107 | "peerDependencies": { 108 | "rollup": "^2.78.0||^3.0.0||^4.0.0" 109 | }, 110 | "peerDependenciesMeta": { 111 | "rollup": { 112 | "optional": true 113 | } 114 | } 115 | }, 116 | "node_modules/@rollup/pluginutils": { 117 | "version": "5.1.0", 118 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", 119 | "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", 120 | "dependencies": { 121 | "@types/estree": "^1.0.0", 122 | "estree-walker": "^2.0.2", 123 | "picomatch": "^2.3.1" 124 | }, 125 | "engines": { 126 | "node": ">=14.0.0" 127 | }, 128 | "peerDependencies": { 129 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 130 | }, 131 | "peerDependenciesMeta": { 132 | "rollup": { 133 | "optional": true 134 | } 135 | } 136 | }, 137 | "node_modules/@rollup/rollup-android-arm-eabi": { 138 | "version": "4.17.2", 139 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", 140 | "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", 141 | "cpu": [ 142 | "arm" 143 | ], 144 | "optional": true, 145 | "os": [ 146 | "android" 147 | ] 148 | }, 149 | "node_modules/@rollup/rollup-android-arm64": { 150 | "version": "4.17.2", 151 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", 152 | "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", 153 | "cpu": [ 154 | "arm64" 155 | ], 156 | "optional": true, 157 | "os": [ 158 | "android" 159 | ] 160 | }, 161 | "node_modules/@rollup/rollup-darwin-arm64": { 162 | "version": "4.17.2", 163 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", 164 | "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", 165 | "cpu": [ 166 | "arm64" 167 | ], 168 | "optional": true, 169 | "os": [ 170 | "darwin" 171 | ] 172 | }, 173 | "node_modules/@rollup/rollup-darwin-x64": { 174 | "version": "4.17.2", 175 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", 176 | "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", 177 | "cpu": [ 178 | "x64" 179 | ], 180 | "optional": true, 181 | "os": [ 182 | "darwin" 183 | ] 184 | }, 185 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 186 | "version": "4.17.2", 187 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", 188 | "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", 189 | "cpu": [ 190 | "arm" 191 | ], 192 | "optional": true, 193 | "os": [ 194 | "linux" 195 | ] 196 | }, 197 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 198 | "version": "4.17.2", 199 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", 200 | "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", 201 | "cpu": [ 202 | "arm" 203 | ], 204 | "optional": true, 205 | "os": [ 206 | "linux" 207 | ] 208 | }, 209 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 210 | "version": "4.17.2", 211 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", 212 | "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", 213 | "cpu": [ 214 | "arm64" 215 | ], 216 | "optional": true, 217 | "os": [ 218 | "linux" 219 | ] 220 | }, 221 | "node_modules/@rollup/rollup-linux-arm64-musl": { 222 | "version": "4.17.2", 223 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", 224 | "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", 225 | "cpu": [ 226 | "arm64" 227 | ], 228 | "optional": true, 229 | "os": [ 230 | "linux" 231 | ] 232 | }, 233 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 234 | "version": "4.17.2", 235 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", 236 | "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", 237 | "cpu": [ 238 | "ppc64" 239 | ], 240 | "optional": true, 241 | "os": [ 242 | "linux" 243 | ] 244 | }, 245 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 246 | "version": "4.17.2", 247 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", 248 | "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", 249 | "cpu": [ 250 | "riscv64" 251 | ], 252 | "optional": true, 253 | "os": [ 254 | "linux" 255 | ] 256 | }, 257 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 258 | "version": "4.17.2", 259 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", 260 | "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", 261 | "cpu": [ 262 | "s390x" 263 | ], 264 | "optional": true, 265 | "os": [ 266 | "linux" 267 | ] 268 | }, 269 | "node_modules/@rollup/rollup-linux-x64-gnu": { 270 | "version": "4.17.2", 271 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", 272 | "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", 273 | "cpu": [ 274 | "x64" 275 | ], 276 | "optional": true, 277 | "os": [ 278 | "linux" 279 | ] 280 | }, 281 | "node_modules/@rollup/rollup-linux-x64-musl": { 282 | "version": "4.17.2", 283 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", 284 | "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", 285 | "cpu": [ 286 | "x64" 287 | ], 288 | "optional": true, 289 | "os": [ 290 | "linux" 291 | ] 292 | }, 293 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 294 | "version": "4.17.2", 295 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", 296 | "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", 297 | "cpu": [ 298 | "arm64" 299 | ], 300 | "optional": true, 301 | "os": [ 302 | "win32" 303 | ] 304 | }, 305 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 306 | "version": "4.17.2", 307 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", 308 | "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", 309 | "cpu": [ 310 | "ia32" 311 | ], 312 | "optional": true, 313 | "os": [ 314 | "win32" 315 | ] 316 | }, 317 | "node_modules/@rollup/rollup-win32-x64-msvc": { 318 | "version": "4.17.2", 319 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", 320 | "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", 321 | "cpu": [ 322 | "x64" 323 | ], 324 | "optional": true, 325 | "os": [ 326 | "win32" 327 | ] 328 | }, 329 | "node_modules/@types/estree": { 330 | "version": "1.0.5", 331 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", 332 | "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" 333 | }, 334 | "node_modules/@types/resolve": { 335 | "version": "1.20.2", 336 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", 337 | "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" 338 | }, 339 | "node_modules/acorn": { 340 | "version": "8.11.3", 341 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", 342 | "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", 343 | "bin": { 344 | "acorn": "bin/acorn" 345 | }, 346 | "engines": { 347 | "node": ">=0.4.0" 348 | } 349 | }, 350 | "node_modules/buffer-from": { 351 | "version": "1.1.2", 352 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 353 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 354 | }, 355 | "node_modules/builtin-modules": { 356 | "version": "3.3.0", 357 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", 358 | "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", 359 | "engines": { 360 | "node": ">=6" 361 | }, 362 | "funding": { 363 | "url": "https://github.com/sponsors/sindresorhus" 364 | } 365 | }, 366 | "node_modules/commander": { 367 | "version": "2.20.3", 368 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 369 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 370 | }, 371 | "node_modules/countries-and-timezones": { 372 | "version": "3.6.0", 373 | "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.6.0.tgz", 374 | "integrity": "sha512-8/nHBCs1eKeQ1jnsZVGdqrLYxS8nPcfJn8PnmxdJXWRLZdXsGFR8gnVhRjatGDBjqmPm7H+FtYpBYTPWd0Eiqg==", 375 | "engines": { 376 | "node": ">=8.x", 377 | "npm": ">=5.x" 378 | } 379 | }, 380 | "node_modules/deepmerge": { 381 | "version": "4.3.1", 382 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 383 | "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 384 | "engines": { 385 | "node": ">=0.10.0" 386 | } 387 | }, 388 | "node_modules/estree-walker": { 389 | "version": "2.0.2", 390 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 391 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" 392 | }, 393 | "node_modules/fsevents": { 394 | "version": "2.3.3", 395 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 396 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 397 | "hasInstallScript": true, 398 | "optional": true, 399 | "os": [ 400 | "darwin" 401 | ], 402 | "engines": { 403 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 404 | } 405 | }, 406 | "node_modules/function-bind": { 407 | "version": "1.1.2", 408 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 409 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 410 | "funding": { 411 | "url": "https://github.com/sponsors/ljharb" 412 | } 413 | }, 414 | "node_modules/hasown": { 415 | "version": "2.0.2", 416 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 417 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 418 | "dependencies": { 419 | "function-bind": "^1.1.2" 420 | }, 421 | "engines": { 422 | "node": ">= 0.4" 423 | } 424 | }, 425 | "node_modules/is-builtin-module": { 426 | "version": "3.2.1", 427 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", 428 | "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", 429 | "dependencies": { 430 | "builtin-modules": "^3.3.0" 431 | }, 432 | "engines": { 433 | "node": ">=6" 434 | }, 435 | "funding": { 436 | "url": "https://github.com/sponsors/sindresorhus" 437 | } 438 | }, 439 | "node_modules/is-core-module": { 440 | "version": "2.13.1", 441 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 442 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 443 | "dependencies": { 444 | "hasown": "^2.0.0" 445 | }, 446 | "funding": { 447 | "url": "https://github.com/sponsors/ljharb" 448 | } 449 | }, 450 | "node_modules/is-module": { 451 | "version": "1.0.0", 452 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 453 | "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" 454 | }, 455 | "node_modules/mini-svg-data-uri": { 456 | "version": "1.4.4", 457 | "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", 458 | "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", 459 | "bin": { 460 | "mini-svg-data-uri": "cli.js" 461 | } 462 | }, 463 | "node_modules/path-parse": { 464 | "version": "1.0.7", 465 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 466 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 467 | }, 468 | "node_modules/picomatch": { 469 | "version": "2.3.1", 470 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 471 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 472 | "engines": { 473 | "node": ">=8.6" 474 | }, 475 | "funding": { 476 | "url": "https://github.com/sponsors/jonschlinkert" 477 | } 478 | }, 479 | "node_modules/prettier": { 480 | "version": "3.3.1", 481 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", 482 | "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", 483 | "bin": { 484 | "prettier": "bin/prettier.cjs" 485 | }, 486 | "engines": { 487 | "node": ">=14" 488 | }, 489 | "funding": { 490 | "url": "https://github.com/prettier/prettier?sponsor=1" 491 | } 492 | }, 493 | "node_modules/resolve": { 494 | "version": "1.22.8", 495 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 496 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 497 | "dependencies": { 498 | "is-core-module": "^2.13.0", 499 | "path-parse": "^1.0.7", 500 | "supports-preserve-symlinks-flag": "^1.0.0" 501 | }, 502 | "bin": { 503 | "resolve": "bin/resolve" 504 | }, 505 | "funding": { 506 | "url": "https://github.com/sponsors/ljharb" 507 | } 508 | }, 509 | "node_modules/rollup": { 510 | "version": "4.17.2", 511 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", 512 | "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", 513 | "dependencies": { 514 | "@types/estree": "1.0.5" 515 | }, 516 | "bin": { 517 | "rollup": "dist/bin/rollup" 518 | }, 519 | "engines": { 520 | "node": ">=18.0.0", 521 | "npm": ">=8.0.0" 522 | }, 523 | "optionalDependencies": { 524 | "@rollup/rollup-android-arm-eabi": "4.17.2", 525 | "@rollup/rollup-android-arm64": "4.17.2", 526 | "@rollup/rollup-darwin-arm64": "4.17.2", 527 | "@rollup/rollup-darwin-x64": "4.17.2", 528 | "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", 529 | "@rollup/rollup-linux-arm-musleabihf": "4.17.2", 530 | "@rollup/rollup-linux-arm64-gnu": "4.17.2", 531 | "@rollup/rollup-linux-arm64-musl": "4.17.2", 532 | "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", 533 | "@rollup/rollup-linux-riscv64-gnu": "4.17.2", 534 | "@rollup/rollup-linux-s390x-gnu": "4.17.2", 535 | "@rollup/rollup-linux-x64-gnu": "4.17.2", 536 | "@rollup/rollup-linux-x64-musl": "4.17.2", 537 | "@rollup/rollup-win32-arm64-msvc": "4.17.2", 538 | "@rollup/rollup-win32-ia32-msvc": "4.17.2", 539 | "@rollup/rollup-win32-x64-msvc": "4.17.2", 540 | "fsevents": "~2.3.2" 541 | } 542 | }, 543 | "node_modules/source-map": { 544 | "version": "0.6.1", 545 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 546 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 547 | "engines": { 548 | "node": ">=0.10.0" 549 | } 550 | }, 551 | "node_modules/source-map-support": { 552 | "version": "0.5.21", 553 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 554 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 555 | "dependencies": { 556 | "buffer-from": "^1.0.0", 557 | "source-map": "^0.6.0" 558 | } 559 | }, 560 | "node_modules/supports-preserve-symlinks-flag": { 561 | "version": "1.0.0", 562 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 563 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 564 | "engines": { 565 | "node": ">= 0.4" 566 | }, 567 | "funding": { 568 | "url": "https://github.com/sponsors/ljharb" 569 | } 570 | }, 571 | "node_modules/terser": { 572 | "version": "5.31.0", 573 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", 574 | "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", 575 | "dependencies": { 576 | "@jridgewell/source-map": "^0.3.3", 577 | "acorn": "^8.8.2", 578 | "commander": "^2.20.0", 579 | "source-map-support": "~0.5.20" 580 | }, 581 | "bin": { 582 | "terser": "bin/terser" 583 | }, 584 | "engines": { 585 | "node": ">=10" 586 | } 587 | } 588 | } 589 | } 590 | -------------------------------------------------------------------------------- /script/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "cursors.js", 7 | "scripts": { 8 | "bundle": "rollup -c", 9 | "minify": "terser ./dist/cursors.js --compress --mangle --output ./dist/cursors.min.js", 10 | "build": "npm run bundle && npm run minify && rm ./dist/cursors.js", 11 | "lint": "prettier --write ." 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@rollup/plugin-image": "^3.0.3", 18 | "@rollup/plugin-node-resolve": "^15.2.3", 19 | "countries-and-timezones": "^3.6.0", 20 | "prettier": "^3.3.1", 21 | "rollup": "^4.17.2", 22 | "terser": "^5.31.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/rollup.config.js: -------------------------------------------------------------------------------- 1 | import image from '@rollup/plugin-image'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | 4 | export default { 5 | input: './src/cursors.js', 6 | output: { 7 | file: './dist/cursors.js', 8 | format: 'iife', 9 | }, 10 | plugins: [image(), nodeResolve()], 11 | }; 12 | -------------------------------------------------------------------------------- /script/src/assets/mac.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/src/assets/tux.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/src/assets/win.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/src/config.js: -------------------------------------------------------------------------------- 1 | export const EVENT_THROTTLE_TIMEOUT_MS = 2000; 2 | export const DEFAULT_API_URL = 'prod-encursors-ypdi.encr.app'; 3 | -------------------------------------------------------------------------------- /script/src/cursors.js: -------------------------------------------------------------------------------- 1 | import macCursor from './assets/mac.svg'; 2 | import windowsCursor from './assets/win.svg'; 3 | import tuxCursor from './assets/tux.svg'; 4 | import { startWS } from './ws'; 5 | import ct from 'countries-and-timezones'; 6 | import { DEFAULT_API_URL } from './config'; 7 | 8 | (async () => { 9 | console.log( 10 | 'Collaborative cursors experience on this page is powered by Encursors. Learn more at:\nhttps://github.com/anfragment/encursors' 11 | ); 12 | 13 | const prefersReducedMotion = 14 | window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || 15 | window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; 16 | if (prefersReducedMotion) { 17 | console.debug('Reduced motion is enabled, not showing cursors'); 18 | return; 19 | } 20 | 21 | const os = getOS(); 22 | if (!os) { 23 | console.debug('Unsupported OS, not showing cursors'); 24 | return; 25 | } 26 | const country = ct.getCountryForTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone); 27 | if (!country) { 28 | console.debug('Could not determine country, not showing cursors'); 29 | return; 30 | } 31 | const countryCode = country.id; 32 | 33 | const scriptTag = document.currentScript; 34 | 35 | const apiURL = scriptTag.getAttribute('data-api-url') || DEFAULT_API_URL; 36 | const zIndex = scriptTag.getAttribute('data-z-index'); 37 | 38 | const url = window.location.href.split('?')[0].split('#')[0]; 39 | 40 | const response = await fetch( 41 | `${url.startsWith('https') ? 'https' : 'http'}://${apiURL}/cursors?url=${url}` 42 | ); 43 | const data = await response.json(); 44 | for (const cursor of data.cursors || []) { 45 | createCursor(cursor); 46 | } 47 | 48 | startWS( 49 | `${url.startsWith('https') ? 'wss' : 'ws'}://${apiURL}/subscribe?url=${url}&country=${countryCode}&os=${os}`, 50 | createCursor, 51 | updateCursor, 52 | deleteCursor 53 | ); 54 | 55 | function createCursor(cursor) { 56 | const el = document.createElement('div'); 57 | el.setAttribute('data-cursor-id', cursor.id); 58 | el.style.position = 'absolute'; 59 | 60 | if (zIndex) { 61 | el.style.zIndex = zIndex; 62 | } 63 | el.style.transition = 'left 0.2s, top 0.2s'; 64 | if (cursor.posX === 0 && cursor.posY === 0) { 65 | el.style.display = 'none'; 66 | } else { 67 | el.style.display = 'flex'; 68 | } 69 | el.style.alignItems = 'center'; 70 | el.style.justifyContent = 'center'; 71 | el.style.pointerEvents = 'none'; 72 | 73 | const img = document.createElement('img'); 74 | switch (cursor.os) { 75 | case 0: 76 | img.src = macCursor; 77 | break; 78 | case 1: 79 | img.src = windowsCursor; 80 | break; 81 | case 2: 82 | img.src = tuxCursor; 83 | break; 84 | default: 85 | img.src = macCursor; 86 | break; 87 | } 88 | img.style.width = '20px'; 89 | img.style.height = '20px'; 90 | el.appendChild(img); 91 | 92 | const countryFlag = document.createElement('div'); 93 | countryFlag.textContent = getFlagEmoji(cursor.country); 94 | countryFlag.style.position = 'relative'; 95 | countryFlag.style.left = '-2px'; 96 | el.appendChild(countryFlag); 97 | 98 | const elWidth = el.clientWidth; 99 | const elHeight = el.clientHeight; 100 | el.style.left = `${boundByDocWidth(cursor.posX, elWidth)}px`; 101 | el.style.top = `${boundByDocHeight(cursor.posY, elHeight)}px`; 102 | 103 | document.body.appendChild(el); 104 | return el; 105 | } 106 | 107 | function updateCursor(cursor) { 108 | let el = document.querySelector(`[data-cursor-id="${cursor.id}"]`); 109 | if (!el) { 110 | return; 111 | } 112 | 113 | const elWidth = el.clientWidth; 114 | const elHeight = el.clientHeight; 115 | el.style.left = `${boundByDocWidth(cursor.posX, elWidth)}px`; 116 | el.style.top = `${boundByDocHeight(cursor.posY, elHeight)}px`; 117 | if (cursor.posX === 0 && cursor.posY === 0) { 118 | el.style.display = 'none'; 119 | } else { 120 | el.style.display = 'flex'; 121 | } 122 | } 123 | 124 | function deleteCursor(cursor) { 125 | const el = document.querySelector(`[data-cursor-id="${cursor.id}"]`); 126 | if (el) { 127 | el.remove(); 128 | } 129 | } 130 | })(); 131 | 132 | function boundByDocHeight(y, elHeight) { 133 | return Math.min(Math.max(0, y), document.documentElement.scrollHeight - 1.5 * elHeight - 1); 134 | } 135 | 136 | function boundByDocWidth(x, elWidth) { 137 | return Math.min(Math.max(0, x), document.documentElement.scrollWidth - 1.5 * elWidth - 1); 138 | } 139 | 140 | function getFlagEmoji(countryCode) { 141 | const codePoints = countryCode 142 | .toUpperCase() 143 | .split('') 144 | .map((char) => 0x1f1e6 + char.charCodeAt(0) - 'A'.charCodeAt(0)); 145 | return String.fromCodePoint(...codePoints); 146 | } 147 | 148 | function getOS() { 149 | const platform = window.navigator?.userAgentData?.platform || window.navigator.platform, 150 | macosPlatforms = ['macOS', 'Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], 151 | windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; 152 | 153 | if (macosPlatforms.indexOf(platform) !== -1) { 154 | return 'mac'; 155 | } else if (windowsPlatforms.indexOf(platform) !== -1) { 156 | return 'win'; 157 | } else if (/Linux/.test(platform)) { 158 | return 'linux'; 159 | } 160 | return null; 161 | } 162 | -------------------------------------------------------------------------------- /script/src/ws.js: -------------------------------------------------------------------------------- 1 | import { EVENT_THROTTLE_TIMEOUT_MS } from './config'; 2 | 3 | export function startWS(url, onCursorEnter, onCursorMove, onCursorLeave) { 4 | let ws = new WebSocket(url); 5 | let open = false; 6 | 7 | let clientX = 0, 8 | clientY = 0; 9 | const onMove = throttle((e) => { 10 | if (typeof e.clientX === 'number') { 11 | clientX = e.clientX; 12 | clientY = e.clientY; 13 | } 14 | if (!open) { 15 | return; 16 | } 17 | const x = Math.floor(clientX + window.scrollX); 18 | const y = Math.floor(clientY + window.scrollY); 19 | ws.send(JSON.stringify([x, y])); 20 | }, EVENT_THROTTLE_TIMEOUT_MS); 21 | document.addEventListener('mousemove', onMove); 22 | document.addEventListener('mouseenter', onMove); 23 | document.addEventListener('scroll', onMove); 24 | 25 | ws.onopen = function () { 26 | open = true; 27 | }; 28 | 29 | ws.onclose = function () { 30 | open = false; 31 | setTimeout(() => { 32 | ws = new WebSocket(url); 33 | }, 5000); 34 | }; 35 | 36 | ws.onmessage = function (event) { 37 | const data = JSON.parse(event.data); 38 | switch (data.type) { 39 | case 'enter': 40 | onCursorEnter(data.payload); 41 | break; 42 | case 'move': 43 | onCursorMove(data.payload); 44 | break; 45 | case 'leave': 46 | onCursorLeave(data.payload); 47 | break; 48 | } 49 | }; 50 | 51 | ws.onerror = function (error) { 52 | console.error('WebSocket error:', error); 53 | }; 54 | } 55 | 56 | function throttle(cb, delay) { 57 | let timeoutId, 58 | lastArgs, 59 | last = 0; 60 | return function (...args) { 61 | lastArgs = args; 62 | if (timeoutId) { 63 | return; 64 | } 65 | 66 | const now = Date.now(); 67 | if (now - last > delay) { 68 | last = now; 69 | cb(...lastArgs); 70 | return; 71 | } 72 | 73 | timeoutId = setTimeout( 74 | () => { 75 | last = Date.now(); 76 | cb(...lastArgs); 77 | timeoutId = null; 78 | }, 79 | delay - (now - last) 80 | ); 81 | }; 82 | } 83 | --------------------------------------------------------------------------------