Set up server to relay ripples and presence info
This commit is contained in:
parent
398aa43338
commit
8ccb60de61
7 changed files with 338 additions and 7 deletions
|
|
@ -1,4 +1,9 @@
|
||||||
const canvas = document.querySelector<HTMLCanvasElement>('#canvas')!
|
const canvas = document.querySelector<HTMLCanvasElement>('#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')!
|
const ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
|
|
@ -14,24 +19,38 @@ type Particle = {
|
||||||
maxRadius: number
|
maxRadius: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const createParticle = (position: [number, number], maxRadius: number): Particle => ({
|
|
||||||
position,
|
|
||||||
maxRadius,
|
|
||||||
age: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
let state = {
|
let state = {
|
||||||
particles: <Particle[]>[]
|
particles: <Particle[]>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = typeof state
|
type State = typeof state
|
||||||
|
|
||||||
canvas.addEventListener('mousedown', (e) => {
|
const createParticle = (position: [number, number], maxRadius: number): Particle => ({
|
||||||
// TODO Normalize x and y coords
|
position,
|
||||||
const particle = createParticle([e.clientX, e.clientY], MAX_RADIUS)
|
maxRadius,
|
||||||
state.particles.push(particle)
|
age: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const makeRipple = (position: [number, number], radius: number): void => {
|
||||||
|
const particle = createParticle(position, radius)
|
||||||
|
socket.send(JSON.stringify(<Message>{
|
||||||
|
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) {
|
function* minDist(dist: number) {
|
||||||
let prev: Point
|
let prev: Point
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -50,11 +69,33 @@ const hasSpace = minDist(70)
|
||||||
canvas.addEventListener('mousemove', (e) => {
|
canvas.addEventListener('mousemove', (e) => {
|
||||||
const shouldSpawn = hasSpace.next().value!
|
const shouldSpawn = hasSpace.next().value!
|
||||||
if (shouldSpawn([e.clientX, e.clientY])) {
|
if (shouldSpawn([e.clientX, e.clientY])) {
|
||||||
const particle = createParticle([e.clientX, e.clientY], MAX_RADIUS / 3*2)
|
const radius = MAX_RADIUS / 3*2
|
||||||
state.particles.push(particle)
|
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 & { type: 'presence-information' }>message
|
||||||
|
console.log(`i'm seeing ${data.others} others around the pond`)
|
||||||
|
} else if (message?.type === 'ripple') {
|
||||||
|
const data = <Message & { type: 'ripple' }>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 => ({
|
const update = (state: State, deltaTime: number): State => ({
|
||||||
particles: state.particles
|
particles: state.particles
|
||||||
.map(p => ({
|
.map(p => ({
|
||||||
|
|
@ -64,6 +105,7 @@ const update = (state: State, deltaTime: number): State => ({
|
||||||
.filter(p => p.age < MAX_AGE)
|
.filter(p => p.age < MAX_AGE)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// draw particles
|
||||||
const render = (state: State, ctx: CanvasRenderingContext2D) => {
|
const render = (state: State, ctx: CanvasRenderingContext2D) => {
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
|
||||||
|
|
||||||
|
|
@ -90,6 +132,7 @@ const loop = () => {
|
||||||
requestAnimationFrame(loop)
|
requestAnimationFrame(loop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start everything
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
canvas.width = canvas.parentElement!.clientWidth
|
canvas.width = canvas.parentElement!.clientWidth
|
||||||
canvas.height = canvas.parentElement!.clientHeight
|
canvas.height = canvas.parentElement!.clientHeight
|
||||||
|
|
|
||||||
175
server/.gitignore
vendored
Normal file
175
server/.gitignore
vendored
Normal file
|
|
@ -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
|
||||||
15
server/README.md
Normal file
15
server/README.md
Normal file
|
|
@ -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.
|
||||||
BIN
server/bun.lockb
Executable file
BIN
server/bun.lockb
Executable file
Binary file not shown.
65
server/index.ts
Normal file
65
server/index.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { type ServerWebSocket } from "bun"
|
||||||
|
|
||||||
|
type WebsocketData = {
|
||||||
|
clientId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message =
|
||||||
|
| { type: 'presence-information', others: number }
|
||||||
|
| { type: 'ripple', position: [number, number], maxRadius: number }
|
||||||
|
|
||||||
|
const clients: Map<WebsocketData['clientId'], ServerWebSocket<WebsocketData>> = new Map()
|
||||||
|
|
||||||
|
const server = Bun.serve<WebsocketData>({
|
||||||
|
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(<Message>{
|
||||||
|
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(<Message>{
|
||||||
|
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}`)
|
||||||
11
server/package.json
Normal file
11
server/package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "@compost.party/pond-server",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
server/tsconfig.json
Normal file
22
server/tsconfig.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue