initial prototype

This commit is contained in:
Peter Millauer
2026-05-09 20:23:46 +02:00
commit 48148573ac
14 changed files with 4510 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+4129
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "ts-canvas-app",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "tsc && npm run build:client",
"build:client": "tsc -p src/client && webpack --config webpack.config.js && cp src/public/* dist/public",
"dev": "npm run build && node dist/server/main.js",
"clean": "rm -rf dist"
},
"dependencies": {
"@types/express": "^5.0.6",
"assert": "^2.1.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.1",
"depents": "^0.0.8",
"express": "^4.19.2",
"http": "^0.0.1-security",
"https-browserify": "^1.0.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"querystring-es3": "^0.2.1",
"rpclibrary": "^2.5.1",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"timers-browserify": "^2.0.12",
"url": "^0.11.4",
"util": "^0.12.5",
"vm-browserify": "^1.1.2"
},
"devDependencies": {
"rolldown": "latest",
"typescript": "latest",
"webpack": "^5.106.2",
"webpack-cli": "^7.0.2"
}
}
+7
View File
@@ -0,0 +1,7 @@
import { ClientStateService } from './services/state/state.client-service'
import { ClientDrawService } from './services/draw/draw.client-service'
const stateService = new ClientStateService()
const drawService = new ClientDrawService(stateService)
drawService.draw()
stateService.connect()
+9
View File
@@ -0,0 +1,9 @@
export type CanvasState = {
cursorP?: Cursor,
cursorK?: Cursor
}
export type Cursor = {
x: number,
y: number,
}
@@ -0,0 +1,82 @@
import { Cursor } from "../../model/canvas-state";
import { ClientStateService } from "../state/state.client-service";
export class ClientDrawService {
private readonly canvasContext: CanvasRenderingContext2D
constructor(
private readonly stateService: ClientStateService,
private readonly canvas: HTMLCanvasElement = document.getElementById("canvas") as HTMLCanvasElement
) {
this.resizeCanvas()
this.setupCanvasEvents()
window.addEventListener("resize", () => this.resizeCanvas());
this.canvasContext = canvas.getContext("2d")!
}
draw() {
const cursors: Cursor[] = this.stateService.getCursors()
this.canvasContext.globalAlpha = 0.05;
this.canvasContext.fillStyle = "white";
this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.canvasContext.globalAlpha = 1;
cursors
.forEach(cursor => {
this.canvasContext.beginPath();
this.canvasContext.arc(cursor.x, cursor.y, 30, 0, Math.PI * 2);
this.canvasContext.fillStyle = "#eaafff";
this.canvasContext.fill();
})
requestAnimationFrame(() => this.draw())
}
private onMouseMove = (e: any) => {
const cursor = this.stateService.get('cursorK')
if (!cursor) {
return
}
const rect = this.canvas.getBoundingClientRect();
this.stateService.set('cursorK', {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
})
};
private onMouseDown = (e: any) => {
const rect = this.canvas.getBoundingClientRect();
this.stateService.set('cursorK', {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
}).then(_ => {
this.canvas.addEventListener("mousemove", this.onMouseMove)
});
}
private setupCanvasEvents() {
this.canvas.addEventListener("mousedown", this.onMouseDown);
this.canvas.addEventListener("mouseup", () => {
this.canvas.removeEventListener('mousemove', this.onMouseMove)
this.stateService.set('cursorK', undefined)
});
this.canvas.removeEventListener("mouseleave", () => {
this.canvas.removeEventListener('mousemove', this.onMouseMove)
this.stateService.set('cursorK', undefined)
});
}
private resizeCanvas() {
const size = Math.min(window.innerWidth, window.innerHeight) * 0.9;
this.canvas.width = size;
this.canvas.height = size;
}
}
@@ -0,0 +1,36 @@
import { RPCSocket } from "../../../../node_modules/rpclibrary/js/Index";
import { CanvasState } from "../../model/canvas-state";
export class ClientStateService {
private remoteService: any
private canvasState: CanvasState = {}
getCursors() {
return [this.canvasState?.cursorK, this.canvasState?.cursorP].filter(cursor => cursor !== undefined)
}
async set<K extends keyof CanvasState>(k: K, v: CanvasState[K]) {
if(v === null || v === undefined){
delete this.canvasState[k]
}
await this.remoteService.set(k, v)
}
get<K extends keyof CanvasState>(k: K): CanvasState[K] {
return this.canvasState[k]
}
async connect() {
const sock = await new RPCSocket(8080, 'localhost').connect();
this.remoteService = sock['StateService']
this.canvasState = await this.remoteService.listen((state: CanvasState) => {
console.log(state)
this.canvasState = state
})
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES6",
"module": "ES6",
"outDir": "../../dist/client",
"rootDir": ".",
"strict": true,
},
"include": ["*.ts"],
}
+26
View File
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Canvas App</title>
<style>
body {
margin: 0;
background: #e0e0e0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
canvas {
background: #ffffff;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module" src="main.js"></script>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
import { Injector } from "depents";
import { ExpressService } from "./services/express/express.service";
Injector.resolve(ExpressService)
@@ -0,0 +1,37 @@
import { Singleton, Initializable, Inject } from "depents";
import http from "http";
import express from "express";
import path from "path";
import { RPCServer } from "rpclibrary";
import { StateService } from "../state/state.service";
@Singleton({
initializationPriority: 1
})
export class ExpressService implements Initializable {
@Inject(StateService)
private stateService: StateService;
initialize() {
const app = express();
const PORT = 8080;
const publicDir = path.join(__dirname, "../../../public");
app.use(express.static(publicDir));
app.get("/", (_req, res) => {
res.sendFile(path.join(publicDir, "index.html"));
});
const httpServer = new http.Server(app)
const rpcServer = new RPCServer([
this.stateService,
])
rpcServer.attach(httpServer)
rpcServer.listen(PORT)
console.log("Server up on", PORT)
};
}
@@ -0,0 +1,47 @@
import { Singleton, Initializable } from "depents";
import { RPCExporter } from "rpclibrary";
import { CanvasState } from "../../../client/model/canvas-state";
@Singleton()
export class StateService implements Initializable, RPCExporter {
name = 'StateService' as const
private canvasState: CanvasState = {}
private clients: Array<(state: any) => void> = []
initialize() {
this.canvasState = {}
this.clients = []
};
set = async <K extends keyof CanvasState>(k: K, v: CanvasState[K]): Promise<void> => {
if (v === null || v === undefined) {
console.log("unset", k)
delete this.canvasState[k]
} else {
console.log("set", k, v)
this.canvasState[k] = v
}
this.clients.forEach((client) => {
client(this.canvasState)
})
}
listen = async (callback: (state: any) => Promise<void>) => {
console.log("New client", this.canvasState)
await callback(this.canvasState)
this.clients = [...this.clients, callback]
return this.canvasState
}
RPCs = [
this.set,
{
name: 'listen' as const,
hook: (cb: any) => { this.listen(cb) }
}
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false,
},
"exclude": [
"scripts"
]
}
+44
View File
@@ -0,0 +1,44 @@
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'development', // or 'production'
entry: "./dist/client/main.js",
output: {
path: path.resolve(__dirname, "dist", "public"),
filename: "main.js"
},
resolve: {
fallback: {
// crypto + compression
crypto: require.resolve('crypto-browserify'),
zlib: require.resolve('browserify-zlib'),
// streams & utilities
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer'),
util: require.resolve('util'),
assert: require.resolve('assert'),
process: require.resolve('process/browser'),
// networking
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
// URL + PATH polyfills
url: require.resolve('url/'),
path: require.resolve('path-browserify'),
querystring: require.resolve('querystring-es3'),
vm: require.resolve("vm-browserify"),
timers: require.resolve("timers-browserify"),
fs: false,
},
},
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
],
};