Peter Millauer 4 дней назад
Сommit
48148573ac

+ 24
- 0
.gitignore Просмотреть файл

1
+# Logs
2
+logs
3
+*.log
4
+npm-debug.log*
5
+yarn-debug.log*
6
+yarn-error.log*
7
+pnpm-debug.log*
8
+lerna-debug.log*
9
+
10
+node_modules
11
+dist
12
+dist-ssr
13
+*.local
14
+
15
+# Editor directories and files
16
+.vscode/*
17
+!.vscode/extensions.json
18
+.idea
19
+.DS_Store
20
+*.suo
21
+*.ntvs*
22
+*.njsproj
23
+*.sln
24
+*.sw?

+ 4129
- 0
package-lock.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 38
- 0
package.json Просмотреть файл

1
+{
2
+  "name": "ts-canvas-app",
3
+  "version": "1.0.0",
4
+  "private": true,
5
+  "scripts": {
6
+    "build": "tsc && npm run build:client",
7
+    "build:client": "tsc -p src/client && webpack --config webpack.config.js && cp src/public/* dist/public",
8
+    "dev": "npm run build && node dist/server/main.js",
9
+    "clean": "rm -rf dist"
10
+  },
11
+  "dependencies": {
12
+    "@types/express": "^5.0.6",
13
+    "assert": "^2.1.0",
14
+    "browserify-zlib": "^0.2.0",
15
+    "buffer": "^6.0.3",
16
+    "crypto-browserify": "^3.12.1",
17
+    "depents": "^0.0.8",
18
+    "express": "^4.19.2",
19
+    "http": "^0.0.1-security",
20
+    "https-browserify": "^1.0.0",
21
+    "path-browserify": "^1.0.1",
22
+    "process": "^0.11.10",
23
+    "querystring-es3": "^0.2.1",
24
+    "rpclibrary": "^2.5.1",
25
+    "stream-browserify": "^3.0.0",
26
+    "stream-http": "^3.2.0",
27
+    "timers-browserify": "^2.0.12",
28
+    "url": "^0.11.4",
29
+    "util": "^0.12.5",
30
+    "vm-browserify": "^1.1.2"
31
+  },
32
+  "devDependencies": {
33
+    "rolldown": "latest",
34
+    "typescript": "latest",
35
+    "webpack": "^5.106.2",
36
+    "webpack-cli": "^7.0.2"
37
+  }
38
+}

+ 7
- 0
src/client/main.ts Просмотреть файл

1
+import { ClientStateService } from './services/state/state.client-service'
2
+import { ClientDrawService } from './services/draw/draw.client-service'
3
+
4
+const stateService = new ClientStateService()
5
+const drawService = new ClientDrawService(stateService)
6
+drawService.draw()
7
+stateService.connect()

+ 9
- 0
src/client/model/canvas-state.ts Просмотреть файл

1
+export type CanvasState = {
2
+    cursorP?: Cursor,
3
+    cursorK?: Cursor
4
+}
5
+
6
+export type Cursor = {
7
+    x: number, 
8
+    y: number,
9
+}

+ 82
- 0
src/client/services/draw/draw.client-service.ts Просмотреть файл

1
+import { Cursor } from "../../model/canvas-state";
2
+import { ClientStateService } from "../state/state.client-service";
3
+
4
+
5
+
6
+export class ClientDrawService {
7
+    private readonly canvasContext: CanvasRenderingContext2D
8
+
9
+    constructor(
10
+
11
+        private readonly stateService: ClientStateService,
12
+        private readonly canvas: HTMLCanvasElement = document.getElementById("canvas") as HTMLCanvasElement
13
+    ) {
14
+        this.resizeCanvas()
15
+        this.setupCanvasEvents()
16
+        window.addEventListener("resize", () => this.resizeCanvas());
17
+        this.canvasContext = canvas.getContext("2d")!
18
+    }
19
+
20
+    draw() {
21
+        const cursors: Cursor[] = this.stateService.getCursors()
22
+        
23
+        this.canvasContext.globalAlpha = 0.05;
24
+        this.canvasContext.fillStyle = "white";
25
+        this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
26
+        this.canvasContext.globalAlpha = 1;
27
+
28
+        cursors
29
+            .forEach(cursor => {
30
+                this.canvasContext.beginPath();
31
+                this.canvasContext.arc(cursor.x, cursor.y, 30, 0, Math.PI * 2);
32
+                this.canvasContext.fillStyle = "#eaafff";
33
+                this.canvasContext.fill();
34
+            })
35
+
36
+        requestAnimationFrame(() => this.draw())
37
+    }
38
+
39
+    private onMouseMove = (e: any) => {
40
+        const cursor = this.stateService.get('cursorK')
41
+        if (!cursor) {
42
+            return
43
+        }
44
+        const rect = this.canvas.getBoundingClientRect();
45
+        this.stateService.set('cursorK', {
46
+            x: e.clientX - rect.left,
47
+            y: e.clientY - rect.top,
48
+        })
49
+    };
50
+
51
+    private onMouseDown = (e: any) => {
52
+        const rect = this.canvas.getBoundingClientRect();
53
+
54
+        this.stateService.set('cursorK', {
55
+            x: e.clientX - rect.left,
56
+            y: e.clientY - rect.top,
57
+        }).then(_ => {
58
+            this.canvas.addEventListener("mousemove", this.onMouseMove)
59
+        });
60
+
61
+    }
62
+
63
+    private setupCanvasEvents() {
64
+        this.canvas.addEventListener("mousedown", this.onMouseDown);
65
+
66
+        this.canvas.addEventListener("mouseup", () => {
67
+            this.canvas.removeEventListener('mousemove', this.onMouseMove)
68
+            this.stateService.set('cursorK', undefined)
69
+        });
70
+
71
+        this.canvas.removeEventListener("mouseleave", () => {
72
+            this.canvas.removeEventListener('mousemove', this.onMouseMove)
73
+            this.stateService.set('cursorK', undefined)
74
+        });
75
+    }
76
+
77
+    private resizeCanvas() {
78
+        const size = Math.min(window.innerWidth, window.innerHeight) * 0.9;
79
+        this.canvas.width = size;
80
+        this.canvas.height = size;
81
+    }
82
+}

+ 36
- 0
src/client/services/state/state.client-service.ts Просмотреть файл

1
+import { RPCSocket } from "../../../../node_modules/rpclibrary/js/Index";
2
+import { CanvasState } from "../../model/canvas-state";
3
+
4
+
5
+export class ClientStateService {
6
+
7
+    private remoteService: any
8
+    private canvasState: CanvasState = {}
9
+
10
+    getCursors() {
11
+        return [this.canvasState?.cursorK, this.canvasState?.cursorP].filter(cursor => cursor !== undefined)
12
+    }
13
+
14
+    async set<K extends keyof CanvasState>(k: K, v: CanvasState[K]) {
15
+        if(v === null || v === undefined){
16
+            delete this.canvasState[k]
17
+        }
18
+        await this.remoteService.set(k, v)
19
+    }
20
+
21
+    get<K extends keyof CanvasState>(k: K): CanvasState[K] {
22
+        return this.canvasState[k]
23
+    }
24
+
25
+    async connect() {
26
+        const sock = await new RPCSocket(8080, 'localhost').connect();
27
+        this.remoteService = sock['StateService']
28
+        this.canvasState = await this.remoteService.listen((state: CanvasState) => {
29
+            console.log(state)
30
+            this.canvasState = state
31
+            
32
+        })
33
+    }
34
+
35
+    
36
+}

+ 10
- 0
src/client/tsconfig.json Просмотреть файл

1
+{
2
+  "compilerOptions": {
3
+    "target": "ES6",
4
+    "module": "ES6",
5
+    "outDir": "../../dist/client",
6
+    "rootDir": ".",
7
+    "strict": true,
8
+  },
9
+  "include": ["*.ts"],
10
+}

+ 26
- 0
src/public/index.html Просмотреть файл

1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+  <meta charset="UTF-8" />
5
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+  <title>Canvas App</title>
7
+  <style>
8
+    body {
9
+      margin: 0;
10
+      background: #e0e0e0;
11
+      display: flex;
12
+      justify-content: center;
13
+      align-items: center;
14
+      height: 100vh;
15
+    }
16
+    canvas {
17
+      background: #ffffff;
18
+      box-shadow: 0 4px 20px rgba(0,0,0,0.1);
19
+    }
20
+  </style>
21
+</head>
22
+<body>
23
+  <canvas id="canvas"></canvas>
24
+  <script type="module" src="main.js"></script>
25
+</body>
26
+</html>

+ 4
- 0
src/server/main.ts Просмотреть файл

1
+import { Injector } from "depents";
2
+import { ExpressService } from "./services/express/express.service";
3
+
4
+Injector.resolve(ExpressService)

+ 37
- 0
src/server/services/express/express.service.ts Просмотреть файл

1
+import { Singleton, Initializable, Inject } from "depents";
2
+import http from "http";
3
+import express from "express";
4
+import path from "path";
5
+import { RPCServer } from "rpclibrary";
6
+import { StateService } from "../state/state.service";
7
+
8
+@Singleton({
9
+    initializationPriority: 1
10
+})
11
+export class ExpressService implements Initializable {
12
+
13
+    @Inject(StateService)
14
+    private stateService: StateService;
15
+
16
+    initialize() {
17
+        const app = express();
18
+        const PORT = 8080;
19
+
20
+        const publicDir = path.join(__dirname, "../../../public");
21
+        app.use(express.static(publicDir));
22
+
23
+        app.get("/", (_req, res) => {
24
+            res.sendFile(path.join(publicDir, "index.html"));
25
+        });
26
+
27
+        const httpServer = new http.Server(app)
28
+        const rpcServer = new RPCServer([
29
+            this.stateService,
30
+
31
+        ])
32
+        rpcServer.attach(httpServer)
33
+        rpcServer.listen(PORT)
34
+
35
+        console.log("Server up on", PORT)
36
+    };
37
+} 

+ 47
- 0
src/server/services/state/state.service.ts Просмотреть файл

1
+import { Singleton, Initializable } from "depents";
2
+import { RPCExporter } from "rpclibrary";
3
+import { CanvasState } from "../../../client/model/canvas-state";
4
+
5
+@Singleton()
6
+export class StateService implements Initializable, RPCExporter {
7
+    name = 'StateService' as const
8
+
9
+
10
+    private canvasState: CanvasState = {}
11
+    private clients: Array<(state: any) => void> = []
12
+
13
+    initialize() {
14
+        this.canvasState = {}
15
+        this.clients = []
16
+    };
17
+
18
+    set = async <K extends keyof CanvasState>(k: K, v: CanvasState[K]): Promise<void> => {
19
+        if (v === null || v === undefined) {
20
+            console.log("unset", k)
21
+            delete this.canvasState[k]
22
+        } else {
23
+            console.log("set", k, v)
24
+            this.canvasState[k] = v
25
+        }
26
+        this.clients.forEach((client) => {
27
+            client(this.canvasState)
28
+        })
29
+
30
+    }
31
+
32
+    listen = async (callback: (state: any) => Promise<void>) => {
33
+        console.log("New client", this.canvasState)
34
+        await callback(this.canvasState)
35
+        this.clients = [...this.clients, callback]
36
+        return this.canvasState
37
+    }
38
+
39
+    RPCs = [
40
+        this.set,
41
+        {
42
+            name: 'listen' as const,
43
+            hook: (cb: any) => { this.listen(cb) }
44
+        }
45
+    ]
46
+
47
+} 

+ 17
- 0
tsconfig.json Просмотреть файл

1
+{
2
+  "compilerOptions": {
3
+    "target": "ES2020",
4
+    "module": "CommonJS",
5
+    "rootDir": "src",
6
+    "outDir": "dist",
7
+    "strict": true,
8
+    "esModuleInterop": true,
9
+    "noFallthroughCasesInSwitch": true,
10
+    "experimentalDecorators": true,
11
+    "emitDecoratorMetadata": true,
12
+    "strictPropertyInitialization": false,
13
+  },
14
+  "exclude": [
15
+    "scripts"
16
+  ]
17
+}

+ 44
- 0
webpack.config.js Просмотреть файл

1
+const path = require('path');
2
+const webpack = require('webpack');
3
+
4
+module.exports = {
5
+    mode: 'development', // or 'production'
6
+    entry: "./dist/client/main.js",
7
+    output: {
8
+        path: path.resolve(__dirname, "dist", "public"),
9
+        filename: "main.js"
10
+    },
11
+    resolve: {
12
+        fallback: {
13
+            // crypto + compression
14
+            crypto: require.resolve('crypto-browserify'),
15
+            zlib: require.resolve('browserify-zlib'),
16
+
17
+            // streams & utilities
18
+            stream: require.resolve('stream-browserify'),
19
+            buffer: require.resolve('buffer'),
20
+            util: require.resolve('util'),
21
+            assert: require.resolve('assert'),
22
+            process: require.resolve('process/browser'),
23
+
24
+            // networking
25
+            http: require.resolve('stream-http'),
26
+            https: require.resolve('https-browserify'),
27
+
28
+            // URL + PATH polyfills
29
+            url: require.resolve('url/'),
30
+            path: require.resolve('path-browserify'),
31
+
32
+            querystring: require.resolve('querystring-es3'),
33
+            vm: require.resolve("vm-browserify"), 
34
+            timers: require.resolve("timers-browserify"),
35
+            fs: false,
36
+        },
37
+    },
38
+    plugins: [
39
+        new webpack.ProvidePlugin({
40
+            process: 'process/browser',
41
+            Buffer: ['buffer', 'Buffer'],
42
+        }),
43
+    ],
44
+};

Загрузка…
Отмена
Сохранить