From 8ccb60de61414ebcab5840c2be67d37be8b7b1d0 Mon Sep 17 00:00:00 2001 From: arne Date: Sat, 27 Jan 2024 12:04:46 +0100 Subject: [PATCH] Set up server to relay ripples and presence info --- client/src/main.ts | 67 ++++++++++++++--- server/.gitignore | 175 +++++++++++++++++++++++++++++++++++++++++++ server/README.md | 15 ++++ server/bun.lockb | Bin 0 -> 3526 bytes server/index.ts | 65 ++++++++++++++++ server/package.json | 11 +++ server/tsconfig.json | 22 ++++++ 7 files changed, 343 insertions(+), 12 deletions(-) create mode 100644 server/.gitignore create mode 100644 server/README.md create mode 100755 server/bun.lockb create mode 100644 server/index.ts create mode 100644 server/package.json create mode 100644 server/tsconfig.json diff --git a/client/src/main.ts b/client/src/main.ts index f086056..d3223fb 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1,4 +1,9 @@ const canvas = document.querySelector('#canvas')! +const socket = new WebSocket(`ws://${window.location.hostname}:3000`) + +type Message = + | { type: 'presence-information', others: number } + | { type: 'ripple', position: [number, number], maxRadius: number } const ctx = canvas.getContext('2d')! @@ -14,24 +19,38 @@ type Particle = { maxRadius: number } -const createParticle = (position: [number, number], maxRadius: number): Particle => ({ - position, - maxRadius, - age: 0, -}) - let state = { particles: [] } type State = typeof state -canvas.addEventListener('mousedown', (e) => { - // TODO Normalize x and y coords - const particle = createParticle([e.clientX, e.clientY], MAX_RADIUS) - state.particles.push(particle) +const createParticle = (position: [number, number], maxRadius: number): Particle => ({ + position, + maxRadius, + age: 0, }) +const makeRipple = (position: [number, number], radius: number): void => { + const particle = createParticle(position, radius) + socket.send(JSON.stringify({ + type: 'ripple', + position: particle.position, + maxRadius: particle.maxRadius, + })) + state.particles.push(particle) +} + +// event handlers + +// create a big ripple when touching the screen +canvas.addEventListener('mousedown', (e) => { + // TODO Normalize x and y coords + const radius = MAX_RADIUS + makeRipple([e.clientX, e.clientY], radius) +}) + +// create a smaller ripple when moving over the screen function* minDist(dist: number) { let prev: Point while (true) { @@ -50,11 +69,33 @@ const hasSpace = minDist(70) canvas.addEventListener('mousemove', (e) => { const shouldSpawn = hasSpace.next().value! if (shouldSpawn([e.clientX, e.clientY])) { - const particle = createParticle([e.clientX, e.clientY], MAX_RADIUS / 3*2) - state.particles.push(particle) + const radius = MAX_RADIUS / 3*2 + makeRipple([e.clientX, e.clientY], radius) } }) +// react to incoming websocket messages +socket.addEventListener('message', (e) => { + console.log('Received websocket message', e) + if (typeof e.data === 'string') { + // TODO: Validate message shape + const message = JSON.parse(e.data) + if (message?.type === 'presence-information') { + const data = message + console.log(`i'm seeing ${data.others} others around the pond`) + } else if (message?.type === 'ripple') { + const data = message + const particle = createParticle(data.position, data.maxRadius) + state.particles.push(particle) + } + } else { + console.warn('i received an odd message and i don\'t know what to do with it', e.data) + } +}) + +// main loop + +// update the particles and remove them after a certain time const update = (state: State, deltaTime: number): State => ({ particles: state.particles .map(p => ({ @@ -64,6 +105,7 @@ const update = (state: State, deltaTime: number): State => ({ .filter(p => p.age < MAX_AGE) }) +// draw particles const render = (state: State, ctx: CanvasRenderingContext2D) => { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) @@ -90,6 +132,7 @@ const loop = () => { requestAnimationFrame(loop) } +// start everything document.addEventListener('DOMContentLoaded', () => { canvas.width = canvas.parentElement!.clientWidth canvas.height = canvas.parentElement!.clientHeight diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..468f82a --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..a1dc637 --- /dev/null +++ b/server/README.md @@ -0,0 +1,15 @@ +# @compost.party/pond-server + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.0.24. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/server/bun.lockb b/server/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..9605651bbfe16d03413268169b76be22bf7568a5 GIT binary patch literal 3526 zcmeHJYfuwc6yBr}V`{~r*wIQ$@Igh*ZW0m*C0eu(M^Q=?WNJW*$tDIv5_UIWR49&A z@li*KqSN|*KX3|+SRX|hD`m#Fv{S7!Qkf}Ql_E36SGCx4m%U~?I>6Ze@JCPP?%8|x zeBV9yo_lZ3;0VggGSp0mi=th*%9&o50-DO{w#>FUoV3d3X0kImRjyhgB?w~j81o+#fj$blH|R>CzYFxMB0FGsf0I3|d2MaRfKx$H)rsl_ZS$J#XNyB_59;MMet*mK z<46AZSRCMs_qGr18eZ8caO)#qsUI z?TGk4775Y8E2frRjy_shrrJ3nHqU$h%GjHc-)&FJC>uX;z9%LBMoDVV{gFQ!*1o>c z|EDzTL9CS3#g&A8D%UCeN>iykE+zkdFvN&?@w%h1W%tsjo5IgUo^G-h1jWssUfeUM zC2B-s-_53zZ1~%>T)84#~u2)Sg`fGhv!lp5G!~9Fb z+18sQTSdHh|AC2+MJ6wgzp;fItJrJG@ox>ES{OQYR`l<+@n_y^yqnAIi``ujaXooW z(%B(YbdgkxEn0nUsclnW-?5+59dB%`{r$e=$XnbW6T zEyO_zl`HHM$c5&4so=*+u(-25Zk8iGnGENnJZ_g&)iDC+^U$n?ad@}{{*{6{dKSld z*mo(4q3sTqV|=8`lbywqZpKapoG2A?RiPZ|NX|YNb$EtO&`*5?Fz?S(X!VFgV0XE# zwD6k8dR> = new Map() + +const server = Bun.serve({ + fetch(req, server) { + // TODO Allow creating private ponds + server.upgrade(req, { + data: { + clientId: crypto.randomUUID() + } + }) + }, + websocket: { + open(ws) { + // register newly connected client and tell them how many other people are there + console.log('Connection opened', ws.data.clientId) + clients.set(ws.data.clientId, ws) + const enterNotice = JSON.stringify({ + type: 'presence-information', + others: clients.size, + }) + for (const [uuid, client] of clients.entries()) { + if (uuid !== ws.data.clientId) { + client.send(enterNotice, true) + } + } + }, + message(ws, message) { + // broadcast message to all other clients + // TODO: Validate message shape + const msg = JSON.parse(`${message}`) + console.log('Relaying message from', ws.data.clientId, msg) + for (const [uuid, client] of clients.entries()) { + if (uuid !== ws.data.clientId) { + client.send(message) + } + } + }, + close(ws, code, reason) { + // remove client from list of registered clients and tell other clients how many people are there + console.log('Connection closed', ws.data.clientId) + clients.delete(ws.data.clientId) + const leaveNotice = JSON.stringify({ + type: 'presence-information', + others: clients.size, + }) + for (const [uuid, client] of clients.entries()) { + if (uuid !== ws.data.clientId) { + client.send(leaveNotice, true) + } + } + } + }, +}) + +console.log(`Server running on ${server.hostname}:${server.port}`) diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..9a61abc --- /dev/null +++ b/server/package.json @@ -0,0 +1,11 @@ +{ + "name": "@compost.party/pond-server", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..dcd8fc5 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true + } +}